From 16dd5164bad9070541f454f57c329a37e10a8c8c Mon Sep 17 00:00:00 2001 From: lucanomax Date: Wed, 21 Oct 2020 07:11:21 +0000 Subject: [PATCH 01/32] Translated using Weblate (Italian) Currently translated at 32.5% (102 of 313 strings) Translation: FluffyChat/Translations-New Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations-new/it/ --- lib/l10n/intl_it.arb | 117 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index c5cbc6f..089134f 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -461,5 +461,122 @@ "@askSSSSCache": { "type": "text", "placeholders": {} + }, + "enterAUsername": "Inserisci un username", + "@enterAUsername": { + "type": "text", + "placeholders": {} + }, + "enterAGroupName": "Inserisci un nome del gruppo", + "@enterAGroupName": { + "type": "text", + "placeholders": {} + }, + "endedTheCall": "{senderName} è entrato in chiamata", + "@endedTheCall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "end2endEncryptionSettings": "Impostazioni crittografia end-to-end", + "@end2endEncryptionSettings": { + "type": "text", + "placeholders": {} + }, + "encryptionNotEnabled": "Crittografia non abilitata", + "@encryptionNotEnabled": { + "type": "text", + "placeholders": {} + }, + "encryptionAlgorithm": "Algoritmo crittografia", + "@encryptionAlgorithm": { + "type": "text", + "placeholders": {} + }, + "encryption": "Crittografia", + "@encryption": { + "type": "text", + "placeholders": {} + }, + "enableEncryptionWarning": "Non potrai disabilitare la crittografia in futuro. Sei sicuro?", + "@enableEncryptionWarning": { + "type": "text", + "placeholders": {} + }, + "enableEmotesGlobally": "Abilita i pacchetti emotes globalmente", + "@enableEmotesGlobally": { + "type": "text", + "placeholders": {} + }, + "emptyChat": "Chat vuota", + "@emptyChat": { + "type": "text", + "placeholders": {} + }, + "emotePacks": "Pacchetti emotes della stanza", + "@emotePacks": { + "type": "text", + "placeholders": {} + }, + "emoteInvalid": "Shortcode emote invalido!", + "@emoteInvalid": { + "type": "text", + "placeholders": {} + }, + "emoteExists": "L'emote già esiste!", + "@emoteExists": { + "type": "text", + "placeholders": {} + }, + "emoteWarnNeedToPick": "Devi scegliere uno shortcode emote e aggiungere un immagine!", + "@emoteWarnNeedToPick": { + "type": "text", + "placeholders": {} + }, + "emoteShortcode": "Shortcode Emotes", + "@emoteShortcode": { + "type": "text", + "placeholders": {} + }, + "emoteSettings": "Impostazioni Emotes", + "@emoteSettings": { + "type": "text", + "placeholders": {} + }, + "editDisplayname": "Modifica nominativo", + "@editDisplayname": { + "type": "text", + "placeholders": {} + }, + "downloadFile": "Scarica file", + "@downloadFile": { + "type": "text", + "placeholders": {} + }, + "displaynameHasBeenChanged": "Il nominativo è stato cambiato", + "@displaynameHasBeenChanged": { + "type": "text", + "placeholders": {} + }, + "discardPicture": "Rimuovi immagine", + "@discardPicture": { + "type": "text", + "placeholders": {} + }, + "devices": "Dispositivi", + "@devices": { + "type": "text", + "placeholders": {} + }, + "device": "Dispositivo", + "@device": { + "type": "text", + "placeholders": {} + }, + "deny": "Declina", + "@deny": { + "type": "text", + "placeholders": {} } } From 844b4a8f9545614d57f23358e1c6b85e948212ca Mon Sep 17 00:00:00 2001 From: Sorunome Date: Fri, 23 Oct 2020 17:36:10 +0200 Subject: [PATCH 02/32] chore: release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d11148..f13a77a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Version 0.20.0 - 2020-??-?? +# Version 0.20.0 - 2020-10-23 ### Features - Added translations: Arabic - Add ability to enable / disable emotes globally From 08e61c0db1354f08576fbdb971bb071a16176327 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 23 Oct 2020 15:45:22 +0000 Subject: [PATCH 03/32] fix: return text field to the previous state after editing message --- CHANGELOG.md | 1 + lib/views/chat.dart | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d11148..5dc25c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Show device name in account information correctly - Fix tapping on aliases / room pills not always working - Link clicking in web not always working +- Return message input field to previous state after editing message - Thanks @inexcode # Version 0.19.0 - 2020-09-21 ### Features diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 50509a3..5d8d7e4 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -98,6 +98,8 @@ class _ChatState extends State<_Chat> { String inputText = ''; + String pendingText = ''; + bool get _canLoadMore => timeline.events.last.type != EventTypes.RoomCreate; void requestHistory() async { @@ -202,12 +204,13 @@ class _ChatState extends State<_Chat> { if (sendController.text.isEmpty) return; room.sendTextEvent(sendController.text, inReplyTo: replyEvent, editEventId: editEvent?.eventId); - sendController.text = ''; + sendController.text = pendingText; setState(() { - inputText = ''; + inputText = pendingText; replyEvent = null; editEvent = null; + pendingText = ''; }); } @@ -522,8 +525,9 @@ class _ChatState extends State<_Chat> { icon: Icon(Icons.edit), onPressed: () { setState(() { + pendingText = sendController.text; editEvent = selectedEvents.first; - sendController.text = editEvent + inputText = sendController.text = editEvent .getDisplayEvent(timeline) .getLocalizedBody(MatrixLocals(L10n.of(context)), withSenderNamePrefix: false, hideReply: true); @@ -787,6 +791,10 @@ class _ChatState extends State<_Chat> { IconButton( icon: Icon(Icons.close), onPressed: () => setState(() { + if (editEvent != null) { + inputText = sendController.text = pendingText; + pendingText = ''; + } replyEvent = null; editEvent = null; }), From e1574e2dab58f05571da021752b8431792e2186a Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 23 Oct 2020 16:33:06 +0000 Subject: [PATCH 04/32] ci(fdroid): Further fdroid deployment fixes --- .gitlab-ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3c08604..ace325c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -152,12 +152,10 @@ upload_to_fdroid_repo: - chmod 700 ~/.ssh - ssh-keyscan -t rsa fdroid.nordgedanken.dev >> ~/.ssh/known_hosts script: - - mkdir -p upload - - cp build/android/* upload/ - cd build/android/ - - export UPDATE_VERSION=$(pcregrep -o1 'version:\\s([0-9]*\\.[0-9]*\\.[0-9]*)\\+[0-9]*' pubspec.yaml) && mv app-release.apk "${UPDATE_VERSION}.apk" + - export UPDATE_VERSION=$(pcregrep -o1 'version:\\s([0-9]*\\.[0-9]*\\.[0-9]*)\\+[0-9]*' ../../pubspec.yaml) && mv app-release.apk "${UPDATE_VERSION}.apk" - rsync -rav -e ssh ./ fluffy@fdroid.nordgedanken.dev:/fdroid/repo - - ssh fluffy@fdroid.nordgedanken.dev "cd fdroid && mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc && fdroid update" + - ssh fluffy@fdroid.nordgedanken.dev "cd fdroid && fdroid update" needs: ["build_android_apk"] only: - tags From c3a989792067567a724bd70b9b9e2872b55e1495 Mon Sep 17 00:00:00 2001 From: Lukas Lihotzki Date: Sat, 24 Oct 2020 00:33:36 +0200 Subject: [PATCH 05/32] feat(linux): wayland support like https://github.com/flutter/flutter/pull/66519/files --- linux/main.cc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/linux/main.cc b/linux/main.cc index 058e617..e7c5c54 100644 --- a/linux/main.cc +++ b/linux/main.cc @@ -1,10 +1,6 @@ #include "my_application.h" int main(int argc, char** argv) { - // Only X11 is currently supported. - // Wayland support is being developed: https://github.com/flutter/flutter/issues/57932. - gdk_set_allowed_backends("x11"); - g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } From 42a927ef5a9ff84da78979a00bceda5fa29f648f Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Sat, 24 Oct 2020 11:31:42 +0000 Subject: [PATCH 06/32] fix: Logo background color --- assets/logo.svg | 91 +++++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/assets/logo.svg b/assets/logo.svg index 93d5231..a3a9516 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,48 +1,43 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + From c7f8a9319b385e8e223fca8ab7efd5cc53d68d34 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Fri, 23 Oct 2020 16:12:11 +0000 Subject: [PATCH 07/32] Translated using Weblate (Russian) Currently translated at 100.0% (313 of 313 strings) Translation: FluffyChat/Translations-New Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations-new/ru/ --- lib/l10n/intl_ru.arb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index d8960c1..fb3952b 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1698,7 +1698,7 @@ "type": "text", "placeholders": {} }, - "deactivateAccountWarning": "Это деактивирует вашу учётную запись пользователя. Это не может быть отменено! Вы уверены?", + "deactivateAccountWarning": "Это деактивирует вашу учётную запись пользователя. Данное действие не может быть отменено! Вы уверены?", "@deactivateAccountWarning": { "type": "text", "placeholders": {} @@ -1708,12 +1708,12 @@ "type": "text", "placeholders": {} }, - "enableEmotesGlobally": "Включить набор эмоджи глобально", + "enableEmotesGlobally": "Включить набор эмодзи глобально", "@enableEmotesGlobally": { "type": "text", "placeholders": {} }, - "emotePacks": "Наборы эмоджи для комнаты", + "emotePacks": "Наборы эмодзи для комнаты", "@emotePacks": { "type": "text", "placeholders": {} From 36405f82160c30f09b5146111421e3901b9bef2d Mon Sep 17 00:00:00 2001 From: Sorunome Date: Sun, 25 Oct 2020 16:59:55 +0100 Subject: [PATCH 08/32] fix: Multiple related store things --- lib/components/matrix.dart | 3 +- lib/components/theme_switcher.dart | 2 +- lib/main.dart | 4 +- lib/utils/famedlysdk_store.dart | 255 +++-------------------------- lib/utils/sentry_controller.dart | 8 +- lib/views/settings.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 8 files changed, 37 insertions(+), 241 deletions(-) diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index f626e5e..47842b5 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -81,8 +81,7 @@ class MatrixState extends State { void clean() async { if (!kIsWeb) return; - final storage = await getLocalStorage(); - await storage.deleteItem(widget.clientName); + await store.deleteItem(widget.clientName); } void _initWithStore() async { diff --git a/lib/components/theme_switcher.dart b/lib/components/theme_switcher.dart index a212b82..dafdd4c 100644 --- a/lib/components/theme_switcher.dart +++ b/lib/components/theme_switcher.dart @@ -175,7 +175,7 @@ class ThemeSwitcherWidgetState extends State { BuildContext context; Future loadSelection(MatrixState matrix) async { - String item = await matrix.store.getItem('theme') ?? 'system'; + var item = await matrix.store.getItem('theme') ?? 'system'; selectedTheme = Themes.values.firstWhere( (e) => e.toString() == 'Themes.' + item, orElse: () => Themes.system); diff --git a/lib/main.dart b/lib/main.dart index a08f001..fecdff2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,8 +21,8 @@ final sentry = SentryClient(dsn: '8591d0d863b646feb4f3dda7e5dcab38'); void captureException(error, stackTrace) async { debugPrint(error.toString()); debugPrint(stackTrace.toString()); - final storage = await getLocalStorage(); - if (storage.getItem('sentry') == true) { + final storage = Store(); + if (await storage.getItem('sentry') == 'true') { await sentry.captureException( exception: error, stackTrace: stackTrace, diff --git a/lib/utils/famedlysdk_store.dart b/lib/utils/famedlysdk_store.dart index 25acf11..6bb3f9c 100644 --- a/lib/utils/famedlysdk_store.dart +++ b/lib/utils/famedlysdk_store.dart @@ -1,27 +1,13 @@ -import 'dart:convert'; - import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:localstorage/localstorage.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:async'; import 'dart:core'; import './database/shared.dart'; -import 'package:olm/olm.dart' as olm; // needed for migration import 'package:random_string/random_string.dart'; -Future getLocalStorage() async { - final directory = PlatformInfos.isBetaDesktop - ? await getApplicationSupportDirectory() - : (PlatformInfos.isWeb ? null : await getApplicationDocumentsDirectory()); - final localStorage = LocalStorage('LocalStorage', directory?.path); - await localStorage.ready; - return localStorage; -} - Future getDatabase(Client client) async { while (_generateDatabaseLock) { await Future.delayed(Duration(milliseconds: 50)); @@ -31,9 +17,9 @@ Future getDatabase(Client client) async { if (_db != null) return _db; final store = Store(); var password = await store.getItem('database-password'); - var needMigration = false; + var newPassword = false; if (password == null || password.isEmpty) { - needMigration = true; + newPassword = true; password = randomString(255); } _db = await constructDb( @@ -41,11 +27,7 @@ Future getDatabase(Client client) async { filename: 'moor.sqlite', password: password, ); - // Check if database is open: - debugPrint((await _db.customSelect('SELECT 1').get()).toString()); - if (needMigration) { - debugPrint('[Moor] Start migration'); - await migrate(client.clientName, _db, store); + if (newPassword) { await store.setItem('database-password', password); } return _db; @@ -57,239 +39,54 @@ Future getDatabase(Client client) async { Database _db; bool _generateDatabaseLock = false; -Future migrate(String clientName, Database db, Store store) async { - debugPrint('[Store] attempting old migration to moor...'); - final oldKeys = await store.getAllItems(); - if (oldKeys == null || oldKeys.isEmpty) { - debugPrint('[Store] empty store!'); - return; // we are done! - } - final credentialsStr = oldKeys[clientName]; - if (credentialsStr == null || credentialsStr.isEmpty) { - debugPrint('[Store] no credentials found!'); - return; // no credentials - } - final Map credentials = json.decode(credentialsStr); - if (!credentials.containsKey('homeserver') || - !credentials.containsKey('token') || - !credentials.containsKey('userID')) { - debugPrint('[Store] invalid credentials!'); - return; // invalid old store, we are done, too! - } - var clientId = 0; - final oldClient = await db.getClient(clientName); - if (oldClient == null) { - clientId = await db.insertClient( - clientName, - credentials['homeserver'], - credentials['token'], - credentials['userID'], - credentials['deviceID'], - credentials['deviceName'], - null, - credentials['olmAccount'], - ); - } else { - clientId = oldClient.clientId; - await db.updateClient( - credentials['homeserver'], - credentials['token'], - credentials['userID'], - credentials['deviceID'], - credentials['deviceName'], - null, - credentials['olmAccount'], - clientId, - ); - } - await db.clearCache(clientId); - debugPrint('[Store] Inserted/updated client, clientId = ${clientId}'); - await db.transaction(() async { - // alright, we stored / updated the client and have the account ID, time to import everything else! - // user_device_keys and user_device_keys_key - debugPrint('[Store] Migrating user device keys...'); - final deviceKeysListString = oldKeys['${clientName}.user_device_keys']; - if (deviceKeysListString != null && deviceKeysListString.isNotEmpty) { - Map rawUserDeviceKeys = - json.decode(deviceKeysListString); - for (final entry in rawUserDeviceKeys.entries) { - final map = entry.value; - await db.storeUserDeviceKeysInfo( - clientId, map['user_id'], map['outdated']); - for (final rawKey in map['device_keys'].entries) { - final jsonVaue = rawKey.value; - await db.storeUserDeviceKey( - clientId, - jsonVaue['user_id'], - jsonVaue['device_id'], - json.encode(jsonVaue), - jsonVaue['verified'], - jsonVaue['blocked']); - } - } - } - for (final entry in oldKeys.entries) { - final key = entry.key; - final value = entry.value; - if (value == null || value.isEmpty) { - continue; - } - // olm_sessions - final olmSessionsMatch = - RegExp(r'^\/clients\/([^\/]+)\/olm-sessions$').firstMatch(key); - if (olmSessionsMatch != null) { - if (olmSessionsMatch[1] != credentials['deviceID']) { - continue; - } - debugPrint('[Store] migrating olm sessions...'); - final identityKey = json.decode(value); - for (final olmKey in identityKey.entries) { - final identKey = olmKey.key; - final sessions = olmKey.value; - for (final pickle in sessions) { - var sess = olm.Session(); - sess.unpickle(credentials['userID'], pickle); - await db.storeOlmSession( - clientId, identKey, sess.session_id(), pickle, null); - sess?.free(); - } - } - } - // outbound_group_sessions - final outboundGroupSessionsMatch = RegExp( - r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/outbound_group_session$') - .firstMatch(key); - if (outboundGroupSessionsMatch != null) { - if (outboundGroupSessionsMatch[1] != credentials['deviceID']) { - continue; - } - final pickle = value; - final roomId = outboundGroupSessionsMatch[2]; - debugPrint( - '[Store] Migrating outbound group sessions for room ${roomId}...'); - final devicesString = oldKeys[ - '/clients/${outboundGroupSessionsMatch[1]}/rooms/${roomId}/outbound_group_session_devices']; - var devices = []; - if (devicesString != null) { - devices = List.from(json.decode(devicesString)); - } - await db.storeOutboundGroupSession( - clientId, - roomId, - pickle, - json.encode(devices), - DateTime.now().millisecondsSinceEpoch, - 0, - ); - } - // session_keys - final sessionKeysMatch = - RegExp(r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/session_keys$') - .firstMatch(key); - if (sessionKeysMatch != null) { - if (sessionKeysMatch[1] != credentials['deviceID']) { - continue; - } - final roomId = sessionKeysMatch[2]; - debugPrint('[Store] Migrating session keys for room ${roomId}...'); - final map = json.decode(value); - for (final entry in map.entries) { - await db.storeInboundGroupSession( - clientId, - roomId, - entry.key, - entry.value['inboundGroupSession'], - json.encode(entry.value['content']), - json.encode(entry.value['indexes']), - null, - null); - } - } - } - }); -} - -// see https://github.com/mogol/flutter_secure_storage/issues/161#issuecomment-704578453 -class AsyncMutex { - Completer _completer; - - Future lock() async { - while (_completer != null) { - await _completer.future; - } - - _completer = Completer(); - } - - void unlock() { - assert(_completer != null); - final completer = _completer; - _completer = null; - completer.complete(); - } -} - class Store { - final LocalStorage storage; + LocalStorage storage; final FlutterSecureStorage secureStorage; - static final _mutex = AsyncMutex(); Store() - : storage = LocalStorage('LocalStorage'), - secureStorage = PlatformInfos.isMobile ? FlutterSecureStorage() : null; + : secureStorage = PlatformInfos.isMobile ? FlutterSecureStorage() : null; - Future getItem(String key) async { - if (!PlatformInfos.isMobile) { + Future _setupLocalStorage() async { + if (storage == null) { + final directory = PlatformInfos.isBetaDesktop + ? await getApplicationSupportDirectory() + : (PlatformInfos.isWeb + ? null + : await getApplicationDocumentsDirectory()); + storage = LocalStorage('LocalStorage', directory?.path); await storage.ready; + } + } + + Future getItem(String key) async { + if (!PlatformInfos.isMobile) { + await _setupLocalStorage(); try { - return await storage.getItem(key); + return await storage.getItem(key).toString(); } catch (_) { return null; } } try { - await _mutex.lock(); return await secureStorage.read(key: key); } catch (_) { return null; - } finally { - _mutex.unlock(); } } Future setItem(String key, String value) async { if (!PlatformInfos.isMobile) { - await storage.ready; + await _setupLocalStorage(); return await storage.setItem(key, value); } - if (value == null) { - return await secureStorage.delete(key: key); - } else { - try { - await _mutex.lock(); - return await secureStorage.write(key: key, value: value); - } finally { - _mutex.unlock(); - } - } + return await secureStorage.write(key: key, value: value); } - Future> getAllItems() async { + Future deleteItem(String key) async { if (!PlatformInfos.isMobile) { - try { - final rawStorage = await getLocalstorage('LocalStorage'); - return json.decode(rawStorage); - } catch (_) { - return {}; - } - } - try { - await _mutex.lock(); - return await secureStorage.readAll(); - } catch (_) { - return {}; - } finally { - _mutex.unlock(); + await _setupLocalStorage(); + return await storage.deleteItem(key); } + return await secureStorage.delete(key: key); } } diff --git a/lib/utils/sentry_controller.dart b/lib/utils/sentry_controller.dart index 970a419..09a3a67 100644 --- a/lib/utils/sentry_controller.dart +++ b/lib/utils/sentry_controller.dart @@ -13,14 +13,14 @@ abstract class SentryController { confirmText: L10n.of(context).ok, cancelText: L10n.of(context).no, ); - final storage = await getLocalStorage(); - await storage.setItem('sentry', enableSentry); + final storage = Store(); + await storage.setItem('sentry', enableSentry.toString()); BotToast.showText(text: L10n.of(context).changesHaveBeenSaved); return; } static Future getSentryStatus() async { - final storage = await getLocalStorage(); - return storage.getItem('sentry') as bool; + final storage = Store(); + return await storage.getItem('sentry') == 'true'; } } diff --git a/lib/views/settings.dart b/lib/views/settings.dart index 8896a39..bd4d4a5 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -185,7 +185,7 @@ class _SettingsState extends State { void deleteWallpaperAction(BuildContext context) async { Matrix.of(context).wallpaper = null; - await Matrix.of(context).store.setItem('chat.fluffy.wallpaper', null); + await Matrix.of(context).store.deleteItem('chat.fluffy.wallpaper'); setState(() => null); } diff --git a/pubspec.lock b/pubspec.lock index cc8eab9..aefa679 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -362,7 +362,7 @@ packages: name: flutter_secure_storage url: "https://pub.dartlang.org" source: hosted - version: "3.3.4" + version: "3.3.5" flutter_slidable: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5dfbe56..b3fb5ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: path_provider: ^1.5.1 webview_flutter: ^0.3.19+9 share: ^0.6.3+5 - flutter_secure_storage: ^3.3.4 + flutter_secure_storage: ^3.3.5 http: ^0.12.0+4 universal_html: ^1.1.12 receive_sharing_intent: ^1.3.3 From 941b211e63a8bb72208621849a121eb9833f59d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kate=C5=99ina=20Churanov=C3=A1?= Date: Sun, 25 Oct 2020 17:34:14 +0100 Subject: [PATCH 09/32] fix: send file dialog - prevent multiple file sending --- lib/components/dialogs/send_file_dialog.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/components/dialogs/send_file_dialog.dart b/lib/components/dialogs/send_file_dialog.dart index 1ad33ef..061c0b7 100644 --- a/lib/components/dialogs/send_file_dialog.dart +++ b/lib/components/dialogs/send_file_dialog.dart @@ -19,7 +19,7 @@ class SendFileDialog extends StatefulWidget { class _SendFileDialogState extends State { bool origImage = false; - + bool _isSending = false; Future _send() async { var file = widget.file; if (file is MatrixImageFile && !origImage) { @@ -82,10 +82,16 @@ class _SendFileDialogState extends State { ), FlatButton( child: Text(L10n.of(context).send), - onPressed: () async { - await SimpleDialogs(context).tryRequestWithLoadingDialog(_send()); - await Navigator.of(context).pop(); - }, + onPressed: _isSending + ? null + : () async { + setState(() { + _isSending = true; + }); + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(_send()); + await Navigator.of(context).pop(); + }, ), ], ); From e917879d92a126d5262fc7d9626fd5c594f01b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kate=C5=99ina=20Churanov=C3=A1?= Date: Sun, 25 Oct 2020 18:36:47 +0100 Subject: [PATCH 10/32] fix: loading spinner stuck on broken images Fixes #198 --- lib/components/avatar.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/components/avatar.dart b/lib/components/avatar.dart index c4b8e16..01e1c60 100644 --- a/lib/components/avatar.dart +++ b/lib/components/avatar.dart @@ -68,6 +68,11 @@ class Avatar extends StatelessWidget { textWidget, ], ), + errorWidget: (c, s, d) => Stack( + children: [ + textWidget, + ], + ), ), ), ), From 2f179d2eae32aad9e53624ab801ec99960254e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=A9c=20Tu=E1=BA=A5n=20Ho=C3=A0ng?= Date: Mon, 26 Oct 2020 07:30:33 +0100 Subject: [PATCH 11/32] Added translation using Weblate (Vietnamese) --- lib/l10n/intl_vi.arb | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/l10n/intl_vi.arb diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/lib/l10n/intl_vi.arb @@ -0,0 +1 @@ +{} From d8e501904a5fb441b206ddcdbdd7f4e2807ef9fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sun, 25 Oct 2020 16:08:23 +0000 Subject: [PATCH 12/32] Translated using Weblate (Turkish) Currently translated at 100.0% (313 of 313 strings) Translation: FluffyChat/Translations-New Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations-new/tr/ --- lib/l10n/intl_tr.arb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index c767ae9..b05f735 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -137,7 +137,7 @@ "targetName": {} } }, - "blockDevice": "Cihazı Engelle", + "blockDevice": "Aygıtı Engelle", "@blockDevice": { "type": "text", "placeholders": {} @@ -323,12 +323,12 @@ "type": "text", "placeholders": {} }, - "compareEmojiMatch": "Karşılaştırın ve aşağıdaki emojilerin diğer cihazdakilerle eşleştiğinden emin olun:", + "compareEmojiMatch": "Karşılaştırın ve aşağıdaki emojilerin diğer aygıttaki emojilerle eşleştiğinden emin olun:", "@compareEmojiMatch": { "type": "text", "placeholders": {} }, - "compareNumbersMatch": "Karşılaştırın ve aşağıdaki numaraların diğer cihazdakilerle eşleştiğinden emin olun:", + "compareNumbersMatch": "Karşılaştırın ve aşağıdaki numaraların diğer aygıttaki numaralarla eşleştiğinden emin olun:", "@compareNumbersMatch": { "type": "text", "placeholders": {} @@ -474,12 +474,12 @@ "type": "text", "placeholders": {} }, - "device": "Cihaz", + "device": "Aygıt", "@device": { "type": "text", "placeholders": {} }, - "devices": "Cihazlar", + "devices": "Aygıtlar", "@devices": { "type": "text", "placeholders": {} @@ -727,7 +727,7 @@ "link": {} } }, - "isDeviceKeyCorrect": "Aşağıdaki cihaz anahtarı doğru mu?", + "isDeviceKeyCorrect": "Aşağıdaki aygıt anahtarı doğru mu?", "@isDeviceKeyCorrect": { "type": "text", "placeholders": {} @@ -983,7 +983,7 @@ "type": "text", "placeholders": {} }, - "participatingUserDevices": "Katılan kullanıcı cihazları", + "participatingUserDevices": "Katılan kullanıcı aygıtları", "@participatingUserDevices": { "type": "text", "placeholders": {} @@ -1069,7 +1069,7 @@ "type": "text", "placeholders": {} }, - "removeAllOtherDevices": "Diğer tüm cihazları kaldır", + "removeAllOtherDevices": "Diğer tüm aygıtları kaldır", "@removeAllOtherDevices": { "type": "text", "placeholders": {} @@ -1081,7 +1081,7 @@ "username": {} } }, - "removeDevice": "Cihazı kaldır", + "removeDevice": "Aygıtı kaldır", "@removeDevice": { "type": "text", "placeholders": {} @@ -1355,12 +1355,12 @@ "targetName": {} } }, - "unblockDevice": "Cihazın Engellemesini Kaldır", + "unblockDevice": "Aygıtın Engellemesini Kaldır", "@unblockDevice": { "type": "text", "placeholders": {} }, - "unknownDevice": "Bilinmeyen cihaz", + "unknownDevice": "Bilinmeyen aygıt", "@unknownDevice": { "type": "text", "placeholders": {} @@ -1718,7 +1718,7 @@ "type": "text", "placeholders": {} }, - "changeDeviceName": "Cihaz adını değiştir", + "changeDeviceName": "Aygıt adını değiştir", "@changeDeviceName": { "type": "text", "placeholders": {} From dae5b687b6de070e1646e08ecab488bb55f7be59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=A9c=20Tu=E1=BA=A5n=20Ho=C3=A0ng?= Date: Mon, 26 Oct 2020 06:38:29 +0000 Subject: [PATCH 13/32] Translated using Weblate (Vietnamese) Currently translated at 5.7% (18 of 313 strings) Translation: FluffyChat/Translations-New Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations-new/vi/ --- lib/l10n/intl_vi.arb | 99 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index 0967ef4..a8a59ba 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -1 +1,98 @@ -{} +{ + "blockDevice": "Thiết bị bị chặn", + "@blockDevice": { + "type": "text", + "placeholders": {} + }, + "askSSSSCache": "Vui lòng nhập cụm mật khẩu hoặc khóa khôi phục để lưu khóa vào bộ nhớ cache.", + "@askSSSSCache": { + "type": "text", + "placeholders": {} + }, + "areYouSure": "Bạn chắc chứ?", + "@areYouSure": { + "type": "text", + "placeholders": {} + }, + "areGuestsAllowedToJoin": "Khách vãng lai có được tham gia không", + "@areGuestsAllowedToJoin": { + "type": "text", + "placeholders": {} + }, + "archivedRoom": "Phòng hội thảo đã lưu trữ", + "@archivedRoom": { + "type": "text", + "placeholders": {} + }, + "archive": "Lưu trữ", + "@archive": { + "type": "text", + "placeholders": {} + }, + "anyoneCanJoin": "Mọi người đều có thể gia nhập", + "@anyoneCanJoin": { + "type": "text", + "placeholders": {} + }, + "answeredTheCall": "{senderName} đã trả lời cuộc gọi", + "@answeredTheCall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "alreadyHaveAnAccount": "Bạn đã có tài khoản?", + "@alreadyHaveAnAccount": { + "type": "text", + "placeholders": {} + }, + "alias": "bí danh", + "@alias": { + "type": "text", + "placeholders": {} + }, + "admin": "Quản trị viên", + "@admin": { + "type": "text", + "placeholders": {} + }, + "addGroupDescription": "Thêm mô tả cho nhóm", + "@addGroupDescription": { + "type": "text", + "placeholders": {} + }, + "activatedEndToEndEncryption": "{username} đã kích hoạt mã hóa đầu cuối 2 chiều", + "@activatedEndToEndEncryption": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "accountInformation": "Thông tin tài khoản", + "@accountInformation": { + "type": "text", + "placeholders": {} + }, + "account": "Tài khoản", + "@account": { + "type": "text", + "placeholders": {} + }, + "acceptedTheInvitation": "{username} đã đồng ý lời mời", + "@acceptedTheInvitation": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "accept": "Đồng ý", + "@accept": { + "type": "text", + "placeholders": {} + }, + "about": "Giới thiệu", + "@about": { + "type": "text", + "placeholders": {} + } +} From 051ec8f913e6532349f0912cac3ca84507c542bb Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Tue, 27 Oct 2020 13:14:10 +0000 Subject: [PATCH 14/32] chore: Only load google services if needed --- README.md | 5 ----- android/app/build.gradle | 4 +++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 092146d..a737f10 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,6 @@ cd fluffychat-flutter sudo apt install ninja-build ``` -* Outcomment the Google Services plugin at the end of the file `android/app/build.gradle`: -``` -// apply plugin: "com.google.gms.google-services" -``` - * Build with: `flutter build apk` ### iOS / iPadOS diff --git a/android/app/build.gradle b/android/app/build.gradle index 36caa49..0ff405a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -87,4 +87,6 @@ dependencies { implementation "net.zetetic:android-database-sqlcipher:4.4.0" // needed for moor_ffi w/ sqlcipher } -apply plugin: "com.google.gms.google-services" +if(file("google-services.json").exists() || System.getenv('CI')){ + apply plugin: 'com.google.gms.google-services' +} \ No newline at end of file From 1894ddb3dbbe26ac92daac67542f4daf1e59cc93 Mon Sep 17 00:00:00 2001 From: Chris Halse Rogers Date: Wed, 28 Oct 2020 05:43:55 +0000 Subject: [PATCH 15/32] Revert "fix: Snapcraft" This reverts commit c1eebc155f82869a3f38c94b89c9d36c402153ab. --- .gitlab-ci.yml | 23 +++++++++++++ snap/gui/fluffychat.desktop | 9 ++++++ snap/gui/fluffychat.png | Bin 0 -> 26538 bytes snap/snapcraft.yaml | 63 ++++++++++++++++++++++++++++-------- 4 files changed, 81 insertions(+), 14 deletions(-) create mode 100755 snap/gui/fluffychat.desktop create mode 100644 snap/gui/fluffychat.png diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ace325c..286f91d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -194,6 +194,29 @@ build_linux: - build/linux/release/bundle/ only: - main + +snap:edge: + stage: publish + image: "cibuilds/snapcraft:core18" + only: + - main + script: + ## Manually install the flutter-dev snap, so we can use the flutter extension + - 'curl -L $(curl -H "X-Ubuntu-Series: 16" "https://api.snapcraft.io/api/v1/snaps/details/flutter?channel=latest/stable" | jq ".download_url" -r) --output flutter.snap' + - sudo mkdir -p /snap/flutter + - sudo unsquashfs -d /snap/flutter/current flutter.snap + - rm -f flutter.snap + - sudo ln -sf /snap/flutter/current/flutter.sh /snap/bin/flutter + - sudo ln -sf /snap/flutter/current/env.sh /snap/bin/env.sh + - snapcraft + - echo $SNAPCRAFT_LOGIN_FILE | base64 --decode --ignore-garbage > snapcraft.login + - snapcraft login --with snapcraft.login + - snapcraft push --release=edge *.snap + - snapcraft logout + artifacts: + paths: + - './*.snap' + when: on_success snap:publish: stage: publish diff --git a/snap/gui/fluffychat.desktop b/snap/gui/fluffychat.desktop new file mode 100755 index 0000000..8861487 --- /dev/null +++ b/snap/gui/fluffychat.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=FluffyChat +GenericName=Matrix Client +Comment=Chat with your friends +Exec=fluffychat +Icon=${SNAP}/meta/gui/fluffychat.png +Terminal=false +Type=Application +Categories=Network;Chat;InstantMessaging; diff --git a/snap/gui/fluffychat.png b/snap/gui/fluffychat.png new file mode 100644 index 0000000000000000000000000000000000000000..7a46e62c34bea2fa86db5e44a10756f7c581524c GIT binary patch literal 26538 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 y_W$+}``^^l>tC+^+g9v Date: Wed, 28 Oct 2020 06:23:50 +0000 Subject: [PATCH 16/32] Krille/remove status feature --- .../list_items/participant_list_item.dart | 165 +++----------- .../list_items/status_list_item.dart | 83 ------- lib/components/matrix.dart | 60 ------ lib/components/user_bottom_sheet.dart | 186 ++++++++++++++++ lib/l10n/intl_en.arb | 20 ++ lib/utils/fluffy_share.dart | 19 ++ lib/utils/presence_extension.dart | 30 ++- lib/utils/user_status.dart | 21 -- lib/views/chat.dart | 42 ++-- lib/views/chat_list.dart | 202 +++++------------- lib/views/new_private_chat.dart | 9 +- lib/views/status_view.dart | 186 ---------------- pubspec.lock | 2 +- 13 files changed, 373 insertions(+), 652 deletions(-) delete mode 100644 lib/components/list_items/status_list_item.dart create mode 100644 lib/components/user_bottom_sheet.dart create mode 100644 lib/utils/fluffy_share.dart delete mode 100644 lib/utils/user_status.dart delete mode 100644 lib/views/status_view.dart diff --git a/lib/components/list_items/participant_list_item.dart b/lib/components/list_items/participant_list_item.dart index 5258f2c..278fb84 100644 --- a/lib/components/list_items/participant_list_item.dart +++ b/lib/components/list_items/participant_list_item.dart @@ -1,66 +1,15 @@ import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; -import 'package:fluffychat/utils/app_route.dart'; -import 'package:fluffychat/views/chat.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../avatar.dart'; -import '../matrix.dart'; +import '../user_bottom_sheet.dart'; class ParticipantListItem extends StatelessWidget { final User user; const ParticipantListItem(this.user); - void participantAction(BuildContext context, String action) async { - switch (action) { - case 'ban': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context).tryRequestWithLoadingDialog(user.ban()); - } - break; - case 'unban': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.unban()); - } - break; - case 'kick': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context).tryRequestWithLoadingDialog(user.kick()); - } - break; - case 'admin': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.setPower(100)); - } - break; - case 'moderator': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.setPower(50)); - } - break; - case 'user': - if (await SimpleDialogs(context).askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.setPower(0)); - } - break; - case 'message': - final roomId = await user.startDirectChat(); - await Navigator.of(context).pushAndRemoveUntil( - AppRoute.defaultRoute( - context, - ChatView(roomId), - ), - (Route r) => r.isFirst); - break; - } - } - @override Widget build(BuildContext context) { var membershipBatch = { @@ -74,87 +23,43 @@ class ParticipantListItem extends StatelessWidget { : user.powerLevel >= 50 ? L10n.of(context).moderator : ''; - var items = >[]; - if (user.id != Matrix.of(context).client.userID) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).sendAMessage), value: 'message'), - ); - } - if (user.canChangePowerLevel && - user.room.ownPowerLevel == 100 && - user.powerLevel != 100) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).makeAnAdmin), value: 'admin'), - ); - } - if (user.canChangePowerLevel && - user.room.ownPowerLevel >= 50 && - user.powerLevel != 50) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).makeAModerator), value: 'moderator'), - ); - } - if (user.canChangePowerLevel && user.powerLevel != 0) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).revokeAllPermissions), value: 'user'), - ); - } - if (user.canKick) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).kickFromChat), value: 'kick'), - ); - } - if (user.canBan && user.membership != Membership.ban) { - items.add( - PopupMenuItem(child: Text(L10n.of(context).banFromChat), value: 'ban'), - ); - } else if (user.canBan && user.membership == Membership.ban) { - items.add( - PopupMenuItem( - child: Text(L10n.of(context).removeExile), value: 'unban'), - ); - } - return PopupMenuButton( - onSelected: (action) => participantAction(context, action), - itemBuilder: (c) => items, - child: ListTile( - title: Row( - children: [ - Text(user.calcDisplayname()), - permissionBatch.isEmpty - ? Container() - : Container( - padding: EdgeInsets.all(4), - margin: EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).secondaryHeaderColor, - borderRadius: BorderRadius.circular(8), - ), - child: Center(child: Text(permissionBatch)), - ), - membershipBatch[user.membership].isEmpty - ? Container() - : Container( - padding: EdgeInsets.all(4), - margin: EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).secondaryHeaderColor, - borderRadius: BorderRadius.circular(8), - ), - child: - Center(child: Text(membershipBatch[user.membership])), - ), - ], + return ListTile( + onTap: () => showModalBottomSheet( + context: context, + builder: (context) => UserBottomSheet( + user: user, ), - subtitle: Text(user.id), - leading: Avatar(user.avatarUrl, user.calcDisplayname()), ), + title: Row( + children: [ + Text(user.calcDisplayname()), + permissionBatch.isEmpty + ? Container() + : Container( + padding: EdgeInsets.all(4), + margin: EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: Theme.of(context).secondaryHeaderColor, + borderRadius: BorderRadius.circular(8), + ), + child: Center(child: Text(permissionBatch)), + ), + membershipBatch[user.membership].isEmpty + ? Container() + : Container( + padding: EdgeInsets.all(4), + margin: EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: Theme.of(context).secondaryHeaderColor, + borderRadius: BorderRadius.circular(8), + ), + child: Center(child: Text(membershipBatch[user.membership])), + ), + ], + ), + subtitle: Text(user.id), + leading: Avatar(user.avatarUrl, user.calcDisplayname()), ); } } diff --git a/lib/components/list_items/status_list_item.dart b/lib/components/list_items/status_list_item.dart deleted file mode 100644 index 1d5f272..0000000 --- a/lib/components/list_items/status_list_item.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/utils/user_status.dart'; -import 'package:fluffychat/views/status_view.dart'; -import 'package:flutter/material.dart'; -import '../avatar.dart'; -import '../matrix.dart'; - -class StatusListItem extends StatelessWidget { - final UserStatus status; - - const StatusListItem(this.status, {Key key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final client = Matrix.of(context).client; - return FutureBuilder( - future: client.getProfileFromUserId(status.userId), - builder: (context, snapshot) { - final profile = - snapshot.data ?? Profile(status.userId.localpart, null); - return InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => StatusView( - status: status, - avatarUrl: profile.avatarUrl, - displayname: profile.displayname, - ), - ), - ), - child: Container( - width: 76, - child: Column( - children: [ - SizedBox(height: 10), - Container( - child: Stack( - children: [ - Avatar(profile.avatarUrl, profile.displayname), - Positioned( - bottom: 0, - right: 0, - child: Container( - width: 10, - height: 10, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Colors.green, - ), - ), - ), - ], - ), - decoration: BoxDecoration( - border: Border.all( - width: 1, - color: Theme.of(context).primaryColor, - ), - borderRadius: BorderRadius.circular(80), - ), - padding: EdgeInsets.all(2), - ), - Padding( - padding: - const EdgeInsets.only(left: 6.0, top: 0.0, right: 6.0), - child: Text( - profile.displayname.trim().split(' ').first, - overflow: TextOverflow.clip, - maxLines: 1, - style: TextStyle( - color: Theme.of(context).textTheme.bodyText2.color, - fontSize: 13, - ), - ), - ), - ], - ), - ), - ); - }); - } -} diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 47842b5..58b7f65 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; import 'package:fluffychat/utils/firebase_controller.dart'; import 'package:fluffychat/utils/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/user_status.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -22,7 +21,6 @@ import '../main.dart'; import '../utils/app_route.dart'; import '../utils/beautify_string_extension.dart'; import '../utils/famedlysdk_store.dart'; -import '../utils/presence_extension.dart'; import '../views/key_verification.dart'; import '../utils/platform_infos.dart'; import 'avatar.dart'; @@ -91,7 +89,6 @@ class MatrixState extends State { await client.connect(); final firstLoginState = await initLoginState; if (firstLoginState == LoginState.logged) { - _cleanUpUserStatus(userStatuses); if (PlatformInfos.isMobile) { await FirebaseController.setupFirebase( this, @@ -123,7 +120,6 @@ class MatrixState extends State { StreamSubscription onNotification; StreamSubscription onFocusSub; StreamSubscription onBlurSub; - StreamSubscription onPresenceSub; void onJitsiCall(EventUpdate eventUpdate) { final event = Event.fromJson( @@ -246,9 +242,6 @@ class MatrixState extends State { importantStateEvents: { 'im.ponies.room_emotes', // we want emotes to work properly }); - onPresenceSub ??= client.onPresence.stream - .where((p) => p.isUserStatus) - .listen(_storeUserStatus); onJitsiCallSub ??= client.onEvent.stream .where((e) => e.type == 'timeline' && @@ -330,64 +323,11 @@ class MatrixState extends State { super.initState(); } - List get userStatuses { - try { - return (client.accountData[userStatusesType].content['user_statuses'] - as List) - .map((json) => UserStatus.fromJson(json)) - .toList(); - } catch (_) {} - return []; - } - - void _storeUserStatus(Presence presence) { - final tmpUserStatuses = List.from(userStatuses); - final currentStatusIndex = - userStatuses.indexWhere((u) => u.userId == presence.senderId); - final newUserStatus = UserStatus() - ..receivedAt = DateTime.now().millisecondsSinceEpoch - ..statusMsg = presence.presence.statusMsg - ..userId = presence.senderId; - if (currentStatusIndex == -1) { - tmpUserStatuses.add(newUserStatus); - } else if (tmpUserStatuses[currentStatusIndex].statusMsg != - presence.presence.statusMsg) { - if (presence.presence.statusMsg.trim().isEmpty) { - tmpUserStatuses.removeAt(currentStatusIndex); - } else { - tmpUserStatuses[currentStatusIndex] = newUserStatus; - } - } else { - return; - } - _cleanUpUserStatus(tmpUserStatuses); - } - - void _cleanUpUserStatus(List tmpUserStatuses) { - final now = DateTime.now().millisecondsSinceEpoch; - tmpUserStatuses - .removeWhere((u) => (now - u.receivedAt) > (1000 * 60 * 60 * 24)); - tmpUserStatuses.sort((a, b) => b.receivedAt.compareTo(a.receivedAt)); - if (tmpUserStatuses.length > 40) { - tmpUserStatuses.removeRange(40, tmpUserStatuses.length); - } - if (tmpUserStatuses != userStatuses) { - client.setAccountData( - client.userID, - userStatusesType, - { - 'user_statuses': tmpUserStatuses.map((i) => i.toJson()).toList(), - }, - ); - } - } - @override void dispose() { onRoomKeyRequestSub?.cancel(); onKeyVerificationRequestSub?.cancel(); onJitsiCallSub?.cancel(); - onPresenceSub?.cancel(); onNotification?.cancel(); onFocusSub?.cancel(); onBlurSub?.cancel(); diff --git a/lib/components/user_bottom_sheet.dart b/lib/components/user_bottom_sheet.dart new file mode 100644 index 0000000..d91c6bc --- /dev/null +++ b/lib/components/user_bottom_sheet.dart @@ -0,0 +1,186 @@ +import 'dart:math'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/views/chat.dart'; +import 'package:flutter/material.dart'; +import 'content_banner.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../utils/presence_extension.dart'; +import 'dialogs/simple_dialogs.dart'; +import 'matrix.dart'; + +class UserBottomSheet extends StatelessWidget { + final User user; + final Function onMention; + + const UserBottomSheet({Key key, @required this.user, this.onMention}) + : super(key: key); + + void participantAction(BuildContext context, String action) async { + switch (action) { + case 'mention': + Navigator.of(context).pop(); + onMention(); + break; + case 'ban': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context).tryRequestWithLoadingDialog(user.ban()); + } + break; + case 'unban': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(user.unban()); + } + break; + case 'kick': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context).tryRequestWithLoadingDialog(user.kick()); + } + break; + case 'admin': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(user.setPower(100)); + } + break; + case 'moderator': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(user.setPower(50)); + } + break; + case 'user': + if (await SimpleDialogs(context).askConfirmation()) { + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(user.setPower(0)); + } + break; + case 'message': + final roomId = await user.startDirectChat(); + await Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute( + context, + ChatView(roomId), + ), + (Route r) => r.isFirst); + break; + } + } + + @override + Widget build(BuildContext context) { + final presence = Matrix.of(context).client.presences[user.id]; + var items = >[]; + + if (onMention != null) { + items.add( + PopupMenuItem(child: Text(L10n.of(context).mention), value: 'mention'), + ); + } + if (user.id != Matrix.of(context).client.userID) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).sendAMessage), value: 'message'), + ); + } + if (user.canChangePowerLevel && + user.room.ownPowerLevel == 100 && + user.powerLevel != 100) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).makeAnAdmin), value: 'admin'), + ); + } + if (user.canChangePowerLevel && + user.room.ownPowerLevel >= 50 && + user.powerLevel != 50) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).makeAModerator), value: 'moderator'), + ); + } + if (user.canChangePowerLevel && user.powerLevel != 0) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).revokeAllPermissions), value: 'user'), + ); + } + if (user.canKick) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).kickFromChat), value: 'kick'), + ); + } + if (user.canBan && user.membership != Membership.ban) { + items.add( + PopupMenuItem(child: Text(L10n.of(context).banFromChat), value: 'ban'), + ); + } else if (user.canBan && user.membership == Membership.ban) { + items.add( + PopupMenuItem( + child: Text(L10n.of(context).removeExile), value: 'unban'), + ); + } + return Center( + child: Container( + width: min(MediaQuery.of(context).size.width, + AdaptivePageLayout.defaultMinWidth * 1.5), + child: SafeArea( + child: Material( + elevation: 4, + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + elevation: 0, + backgroundColor: + Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5), + leading: IconButton( + icon: Icon(Icons.arrow_downward_outlined), + onPressed: Navigator.of(context).pop, + ), + title: Text(user.calcDisplayname()), + actions: [ + if (user.id != Matrix.of(context).client.userID) + PopupMenuButton( + itemBuilder: (_) => items, + onSelected: (action) => + participantAction(context, action), + ), + ], + ), + body: ListView( + children: [ + ContentBanner( + user.avatarUrl, + defaultIcon: Icons.person_outline, + ), + ListTile( + title: Text(L10n.of(context).username), + subtitle: Text(user.id), + trailing: Icon(Icons.share), + onTap: () => FluffyShare.share(user.id, context), + ), + if (presence != null) + ListTile( + title: Text(presence.getLocalizedStatusMessage(context)), + subtitle: + Text(presence.getLocalizedLastActiveAgo(context)), + trailing: Icon(Icons.circle, + color: presence.presence.currentlyActive + ? Colors.green + : Colors.grey), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 35ceddd..73462e1 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -895,6 +895,11 @@ "type": "text", "placeholders": {} }, + "mention": "Mention", + "@mention": { + "type": "text", + "placeholders": {} + }, "messageWillBeRemovedWarning": "Message will be removed for all participants", "@messageWillBeRemovedWarning": { "type": "text", @@ -992,6 +997,21 @@ "type": "text", "placeholders": {} }, + "online": "Online", + "@online": { + "type": "text", + "placeholders": {} + }, + "offline": "Offline", + "@offline": { + "type": "text", + "placeholders": {} + }, + "unavailable": "Unavailable", + "@unavailable": { + "type": "text", + "placeholders": {} + }, "onlineKeyBackupEnabled": "Online Key Backup is enabled", "@onlineKeyBackupEnabled": { "type": "text", diff --git a/lib/utils/fluffy_share.dart b/lib/utils/fluffy_share.dart new file mode 100644 index 0000000..88409a3 --- /dev/null +++ b/lib/utils/fluffy_share.dart @@ -0,0 +1,19 @@ +import 'package:bot_toast/bot_toast.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:share/share.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +abstract class FluffyShare { + static Future share(String text, BuildContext context) async { + if (PlatformInfos.isMobile) { + return Share.share(text); + } + await Clipboard.setData( + ClipboardData(text: text), + ); + BotToast.showText(text: L10n.of(context).copiedToClipboard); + return; + } +} diff --git a/lib/utils/presence_extension.dart b/lib/utils/presence_extension.dart index bfbd032..22ea01b 100644 --- a/lib/utils/presence_extension.dart +++ b/lib/utils/presence_extension.dart @@ -4,21 +4,37 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'date_time_extension.dart'; +extension on PresenceType { + String getLocalized(BuildContext context) { + switch (this) { + case PresenceType.online: + return L10n.of(context).online; + case PresenceType.unavailable: + return L10n.of(context).unavailable; + case PresenceType.offline: + default: + return L10n.of(context).offline; + } + } +} + extension PresenceExtension on Presence { - bool get isUserStatus => presence?.statusMsg?.isNotEmpty ?? false; + String getLocalizedLastActiveAgo(BuildContext context) { + if (presence.lastActiveAgo != null && presence.lastActiveAgo != 0) { + return L10n.of(context).lastActiveAgo(DateTime.fromMillisecondsSinceEpoch( + DateTime.now().millisecondsSinceEpoch - presence.lastActiveAgo) + .localizedTimeShort(context)); + } + return L10n.of(context).lastSeenLongTimeAgo; + } String getLocalizedStatusMessage(BuildContext context) { if (presence.statusMsg?.isNotEmpty ?? false) { return presence.statusMsg; } - if (presence.lastActiveAgo != null ?? presence.lastActiveAgo != 0) { - return L10n.of(context).lastActiveAgo( - DateTime.fromMillisecondsSinceEpoch(presence.lastActiveAgo) - .localizedTimeShort(context)); - } if (presence.currentlyActive) { return L10n.of(context).currentlyActive; } - return L10n.of(context).lastSeenLongTimeAgo; + return presence.presence.getLocalized(context); } } diff --git a/lib/utils/user_status.dart b/lib/utils/user_status.dart deleted file mode 100644 index dadfb3f..0000000 --- a/lib/utils/user_status.dart +++ /dev/null @@ -1,21 +0,0 @@ -class UserStatus { - String statusMsg; - String userId; - int receivedAt; - - UserStatus(); - - UserStatus.fromJson(Map json) { - statusMsg = json['status_msg']; - userId = json['user_id']; - receivedAt = json['received_at']; - } - - Map toJson() { - final data = {}; - data['status_msg'] = statusMsg; - data['user_id'] = userId; - data['received_at'] = receivedAt; - return data; - } -} diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 5d8d7e4..e7a0f31 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/components/encryption_button.dart'; import 'package:fluffychat/components/list_items/message.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/components/reply_content.dart'; +import 'package:fluffychat/components/user_bottom_sheet.dart'; import 'package:fluffychat/config/app_emojis.dart'; import 'package:fluffychat/utils/app_route.dart'; import 'package:fluffychat/utils/matrix_locals.dart'; @@ -476,16 +477,22 @@ class _ChatState extends State<_Chat> { return ListTile( leading: Avatar(room.avatar, room.displayname), contentPadding: EdgeInsets.zero, - onTap: room.isDirectChat && room.directChatPresence == null - ? null - : room.isDirectChat - ? null - : () => Navigator.of(context).push( - AppRoute.defaultRoute( - context, - ChatDetails(room), - ), - ), + onTap: room.isDirectChat + ? () => showModalBottomSheet( + context: context, + builder: (context) => UserBottomSheet( + user: room + .getUserByMXIDSync(room.directChatMatrixID), + onMention: () => sendController.text += + ' ${room.directChatMatrixID}', + ), + ) + : () => Navigator.of(context).push( + AppRoute.defaultRoute( + context, + ChatDetails(room), + ), + ), title: Text( room.getLocalizedDisplayname( MatrixLocals(L10n.of(context))), @@ -684,10 +691,17 @@ class _ChatState extends State<_Chat> { onSwipe: (direction) => replyAction( replyTo: filteredEvents[i - 1]), child: Message(filteredEvents[i - 1], - onAvatarTab: (Event event) { - sendController.text += - ' ${event.senderId}'; - }, + onAvatarTab: (Event event) => + showModalBottomSheet( + context: context, + builder: (context) => + UserBottomSheet( + user: event.sender, + onMention: () => + sendController.text += + ' ${event.senderId}', + ), + ), onSelect: (Event event) { if (!event.redacted) { if (selectedEvents diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart index d56c3d4..e1f63af 100644 --- a/lib/views/chat_list.dart +++ b/lib/views/chat_list.dart @@ -3,18 +3,15 @@ import 'dart:io'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; -import 'package:fluffychat/components/avatar.dart'; import 'package:fluffychat/components/connection_status_header.dart'; import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; -import 'package:fluffychat/components/list_items/status_list_item.dart'; import 'package:fluffychat/components/list_items/public_room_list_item.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/views/status_view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:share/share.dart'; import '../components/adaptive_page_layout.dart'; import '../components/list_items/chat_list_item.dart'; @@ -198,29 +195,23 @@ class _ChatListState extends State { ); } - void _setStatus(BuildContext context, {bool fromDrawer = false}) async { - if (fromDrawer) Navigator.of(context).pop(); - final ownProfile = await SimpleDialogs(context) - .tryRequestWithLoadingDialog(Matrix.of(context).client.ownProfile); - String composeText; - if (Matrix.of(context).shareContent != null && - Matrix.of(context).shareContent['msgtype'] == 'm.text') { - composeText = Matrix.of(context).shareContent['body']; - Matrix.of(context).shareContent = null; - } - if (ownProfile is Profile) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => StatusView( - composeMode: true, - avatarUrl: ownProfile.avatarUrl, - displayname: ownProfile.displayname ?? - Matrix.of(context).client.userID.localpart, - composeText: composeText, - ), - ), - ); - } + void _setStatus(BuildContext context) async { + Navigator.of(context).pop(); + final statusMsg = await SimpleDialogs(context).enterText( + titleText: L10n.of(context).setStatus, + labelText: L10n.of(context).setStatus, + hintText: L10n.of(context).statusExampleMessage, + multiLine: true, + ); + if (statusMsg?.isEmpty ?? true) return; + final client = Matrix.of(context).client; + await SimpleDialogs(context).tryRequestWithLoadingDialog( + client.sendPresence( + client.userID, + PresenceType.online, + statusMsg: statusMsg, + ), + ); return; } @@ -302,8 +293,7 @@ class _ChatListState extends State { ListTile( leading: Icon(Icons.edit), title: Text(L10n.of(context).setStatus), - onTap: () => - _setStatus(context, fromDrawer: true), + onTap: () => _setStatus(context), ), Divider(height: 1), ListTile( @@ -338,9 +328,11 @@ class _ChatListState extends State { title: Text(L10n.of(context).inviteContact), onTap: () { Navigator.of(context).pop(); - Share.share(L10n.of(context).inviteText( - Matrix.of(context).client.userID, - 'https://matrix.to/#/${Matrix.of(context).client.userID}')); + FluffyShare.share( + L10n.of(context).inviteText( + Matrix.of(context).client.userID, + 'https://matrix.to/#/${Matrix.of(context).client.userID}'), + context); }, ), ], @@ -422,31 +414,14 @@ class _ChatListState extends State { ), floatingActionButton: AdaptivePageLayout.columnMode(context) ? null - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - FloatingActionButton( - heroTag: null, - child: Icon( - Icons.edit, - color: Theme.of(context).primaryColor, - ), - elevation: 1, - backgroundColor: - Theme.of(context).secondaryHeaderColor, - onPressed: () => _setStatus(context), - ), - SizedBox(height: 16.0), - FloatingActionButton( - child: Icon(Icons.add), - backgroundColor: Theme.of(context).primaryColor, - onPressed: () => Navigator.of(context) - .pushAndRemoveUntil( - AppRoute.defaultRoute( - context, NewPrivateChatView()), - (r) => r.isFirst), - ), - ], + : FloatingActionButton( + child: Icon(Icons.add), + backgroundColor: Theme.of(context).primaryColor, + onPressed: () => Navigator.of(context) + .pushAndRemoveUntil( + AppRoute.defaultRoute( + context, NewPrivateChatView()), + (r) => r.isFirst), ), body: Column( children: [ @@ -506,94 +481,28 @@ class _ChatListState extends State { final totalCount = rooms.length + publicRoomsCount; return ListView.separated( - controller: _scrollController, - separatorBuilder: (BuildContext context, - int i) => - i == totalCount - publicRoomsCount - ? ListTile( - title: Text( - L10n.of(context) - .publicRooms + - ':', - style: TextStyle( - fontWeight: - FontWeight.bold, - color: Theme.of(context) - .primaryColor, - ), + controller: _scrollController, + separatorBuilder: (BuildContext context, + int i) => + i == totalCount - publicRoomsCount + ? ListTile( + title: Text( + L10n.of(context) + .publicRooms + + ':', + style: TextStyle( + fontWeight: + FontWeight.bold, + color: Theme.of(context) + .primaryColor, ), - ) - : Container(), - itemCount: totalCount + 1, - itemBuilder: - (BuildContext context, int i) { - if (i == 0) { - final displayPresences = - selectMode != SelectMode.share; - final displayShareStatus = - selectMode == - SelectMode.share && - Matrix.of(context) - .shareContent[ - 'msgtype'] == - 'm.text'; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedContainer( - duration: Duration( - milliseconds: 300), - height: displayPresences - ? 78 - : displayShareStatus - ? 56 - : 0, - child: displayPresences - ? ListView.builder( - scrollDirection: - Axis.horizontal, - itemCount: - Matrix.of(context) - .userStatuses - .length, - itemBuilder: (BuildContext - context, - int i) => - StatusListItem(Matrix - .of(context) - .userStatuses[i]), - ) - : displayShareStatus - ? ListTile( - leading: - CircleAvatar( - radius: Avatar - .defaultSize / - 2, - backgroundColor: - Theme.of( - context) - .secondaryHeaderColor, - child: Icon( - Icons.edit, - color: Theme.of( - context) - .primaryColor, - ), - ), - title: Text(L10n.of( - context) - .setStatus), - onTap: () => - _setStatus( - context)) - : null, - ), - ], - ); - } - i--; - return i < rooms.length + ), + ) + : Container(), + itemCount: totalCount, + itemBuilder: (BuildContext context, + int i) => + i < rooms.length ? ChatListItem( rooms[i], selected: _selectedRoomIds @@ -614,8 +523,9 @@ class _ChatListState extends State { ) : PublicRoomListItem( publicRoomsResponse - .chunk[i - rooms.length]); - }); + .chunk[i - rooms.length], + ), + ); } else { return Center( child: CircularProgressIndicator(), diff --git a/lib/views/new_private_chat.dart b/lib/views/new_private_chat.dart index 46e9fd3..66b9da4 100644 --- a/lib/views/new_private_chat.dart +++ b/lib/views/new_private_chat.dart @@ -7,9 +7,9 @@ import 'package:fluffychat/components/avatar.dart'; import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:share/share.dart'; import 'chat.dart'; import 'chat_list.dart'; @@ -204,9 +204,10 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { Icons.share, size: 16, ), - onTap: () => Share.share(L10n.of(context).inviteText( - Matrix.of(context).client.userID, - 'https://matrix.to/#/${Matrix.of(context).client.userID}')), + onTap: () => FluffyShare.share( + L10n.of(context).inviteText(Matrix.of(context).client.userID, + 'https://matrix.to/#/${Matrix.of(context).client.userID}'), + context), title: Text( '${L10n.of(context).yourOwnUsername}:', style: TextStyle( diff --git a/lib/views/status_view.dart b/lib/views/status_view.dart deleted file mode 100644 index 0ea3ba2..0000000 --- a/lib/views/status_view.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/components/avatar.dart'; -import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; -import 'package:fluffychat/components/matrix.dart'; -import 'package:fluffychat/utils/url_launcher.dart'; -import 'package:fluffychat/utils/user_status.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/utils/app_route.dart'; -import 'package:fluffychat/utils/string_color.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix_link_text/link_text.dart'; - -import 'chat.dart'; - -class StatusView extends StatelessWidget { - final Uri avatarUrl; - final String displayname; - final UserStatus status; - final bool composeMode; - final String composeText; - final TextEditingController _composeController; - - StatusView({ - this.composeMode = false, - this.status, - this.avatarUrl, - this.displayname, - this.composeText, - Key key, - }) : _composeController = TextEditingController(text: composeText), - super(key: key); - - void _sendMessageAction(BuildContext context) async { - final roomId = await User( - status.userId, - room: Room(id: '', client: Matrix.of(context).client), - ).startDirectChat(); - await Navigator.of(context).pushAndRemoveUntil( - AppRoute.defaultRoute( - context, - ChatView(roomId), - ), - (Route r) => r.isFirst); - } - - void _setStatusAction(BuildContext context) async { - if (_composeController.text.isEmpty) return; - await SimpleDialogs(context).tryRequestWithLoadingDialog( - Matrix.of(context).client.sendPresence( - Matrix.of(context).client.userID, PresenceType.online, - statusMsg: _composeController.text), - ); - await Navigator.of(context).popUntil((Route r) => r.isFirst); - } - - void _removeStatusAction(BuildContext context) async { - final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( - Matrix.of(context).client.sendPresence( - Matrix.of(context).client.userID, - PresenceType.online, - statusMsg: - ' ', // Send this empty String make sure that all other devices will get an update - ), - ); - if (success == false) return; - await Navigator.of(context).popUntil((Route r) => r.isFirst); - } - - @override - Widget build(BuildContext context) { - if (composeMode == false && status == null) { - throw ('If composeMode is null then the presence must be not null!'); - } - final padding = const EdgeInsets.only( - top: 16.0, - right: 16.0, - left: 16.0, - bottom: 64.0, - ); - return Scaffold( - backgroundColor: displayname.color, - extendBody: true, - appBar: AppBar( - titleSpacing: 0.0, - brightness: Brightness.dark, - leading: IconButton( - icon: Icon( - Icons.close, - color: Colors.white, - ), - onPressed: Navigator.of(context).pop, - ), - backgroundColor: Colors.transparent, - elevation: 1, - title: ListTile( - contentPadding: EdgeInsets.zero, - leading: Avatar(avatarUrl, displayname), - title: Text( - displayname, - style: TextStyle(color: Colors.white), - ), - subtitle: Text( - status?.userId ?? Matrix.of(context).client.userID, - maxLines: 1, - style: TextStyle(color: Colors.white), - ), - ), - actions: - !composeMode && status.userId == Matrix.of(context).client.userID - ? [ - IconButton( - icon: Icon(Icons.archive), - onPressed: () => _removeStatusAction(context), - color: Colors.white, - ), - ] - : null, - ), - body: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - displayname.color, - Theme.of(context).primaryColor, - displayname.color, - ], - ), - ), - child: composeMode - ? Padding( - padding: padding, - child: TextField( - controller: _composeController, - autofocus: true, - minLines: 1, - maxLines: 20, - style: TextStyle( - fontSize: 30, - color: Colors.white, - ), - textAlign: TextAlign.center, - decoration: InputDecoration( - border: InputBorder.none, - ), - ), - ) - : ListView( - shrinkWrap: true, - padding: padding, - children: [ - LinkText( - text: status.statusMsg, - textAlign: TextAlign.center, - textStyle: TextStyle( - fontSize: 30, - color: Colors.white, - ), - linkStyle: TextStyle( - fontSize: 30, - color: Colors.white70, - decoration: TextDecoration.underline, - ), - onLinkTap: (url) => UrlLauncher(context, url).launchUrl(), - ), - ], - ), - ), - floatingActionButton: - !composeMode && status.userId == Matrix.of(context).client.userID - ? null - : FloatingActionButton.extended( - backgroundColor: Theme.of(context).primaryColor, - icon: Icon(composeMode ? Icons.edit : Icons.message_outlined), - label: Text(composeMode - ? L10n.of(context).setStatus - : L10n.of(context).sendAMessage), - onPressed: () => composeMode - ? _setStatusAction(context) - : _sendMessageAction(context), - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index aefa679..17ea035 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1095,5 +1095,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.10.0-110 <=2.11.0-161.0.dev" + dart: ">=2.10.0-110 <2.11.0" flutter: ">=1.20.0 <2.0.0" From a8b617e09660f8ae1f934a4b2d1185d7c5f58da1 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 07:35:38 +0100 Subject: [PATCH 17/32] fix: Avatar Border Radius --- lib/components/avatar.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/components/avatar.dart b/lib/components/avatar.dart index 01e1c60..0a35c1c 100644 --- a/lib/components/avatar.dart +++ b/lib/components/avatar.dart @@ -45,10 +45,12 @@ class Avatar extends StatelessWidget { ), ); final noPic = mxContent == null || mxContent.toString().isEmpty; + final borderRadius = BorderRadius.circular(size / 2); return InkWell( onTap: onTap, + borderRadius: borderRadius, child: ClipRRect( - borderRadius: BorderRadius.circular(size / 2), + borderRadius: borderRadius, child: Container( width: size, height: size, From 8a542bf5c2f5dd2cd275111074f29b71b2b11dd7 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 08:05:10 +0100 Subject: [PATCH 18/32] fix: Android Download --- android/app/build.gradle | 6 ++---- lib/utils/matrix_file_extension.dart | 12 +++++++++--- lib/utils/platform_infos.dart | 3 +++ pubspec.lock | 21 +++++++++++++++++++++ pubspec.yaml | 2 ++ 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 0ff405a..8ff431e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { applicationId "chat.fluffy.fluffychat" - minSdkVersion 18 + minSdkVersion 21 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -87,6 +87,4 @@ dependencies { implementation "net.zetetic:android-database-sqlcipher:4.4.0" // needed for moor_ffi w/ sqlcipher } -if(file("google-services.json").exists() || System.getenv('CI')){ - apply plugin: 'com.google.gms.google-services' -} \ No newline at end of file +apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/lib/utils/matrix_file_extension.dart b/lib/utils/matrix_file_extension.dart index 2f1dc44..d7daef3 100644 --- a/lib/utils/matrix_file_extension.dart +++ b/lib/utils/matrix_file_extension.dart @@ -1,11 +1,14 @@ import 'dart:io'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart'; import 'package:open_file/open_file.dart'; import 'package:path_provider/path_provider.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:mime_type/mime_type.dart'; +import 'package:downloads_path_provider_28/downloads_path_provider_28.dart'; +import 'package:permission_handler/permission_handler.dart'; extension MatrixFileExtension on MatrixFile { void open() async { @@ -24,9 +27,12 @@ extension MatrixFileExtension on MatrixFile { element.click(); element.remove(); } else { - final downloadsDir = Platform.isAndroid - ? (await getExternalStorageDirectory()) - : (await getApplicationDocumentsDirectory()); + if (!(await Permission.storage.request()).isGranted) return; + final downloadsDir = PlatformInfos.isDesktop + ? (await getDownloadsDirectory()) + : Platform.isAndroid + ? (await DownloadsPathProvider.downloadsDirectory) + : (await getApplicationDocumentsDirectory()); final file = File(downloadsDir.path + '/' + name.split('/').last); file.writeAsBytesSync(bytes); diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index 5473a8c..3bc4898 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -11,5 +11,8 @@ abstract class PlatformInfos { static bool get isBetaDesktop => !kIsWeb && (Platform.isWindows || Platform.isLinux); + static bool get isDesktop => + !kIsWeb && (Platform.isLinux || Platform.isWindows || Platform.isMacOS); + static bool get usesTouchscreen => !isMobile; } diff --git a/pubspec.lock b/pubspec.lock index 17ea035..351bde9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -183,6 +183,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.3" + downloads_path_provider_28: + dependency: "direct main" + description: + name: downloads_path_provider_28 + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" encrypt: dependency: transitive description: @@ -683,6 +690,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.10.0-nullsafety.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1+1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b3fb5ca..3c2919b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,8 @@ dependencies: # desktop_notifications: ^0.0.0-dev.4 // Currently blocked by: https://github.com/canonical/desktop_notifications.dart/issues/5 matrix_link_text: ^0.3.1 path_provider: ^1.5.1 + downloads_path_provider_28: ^0.1.0 + permission_handler: ^5.0.1+1 webview_flutter: ^0.3.19+9 share: ^0.6.3+5 flutter_secure_storage: ^3.3.5 From 87737705ff7d31fc34018caa8ac4bf536fd08a5d Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 08:24:49 +0100 Subject: [PATCH 19/32] chore: Update sdk --- lib/components/matrix.dart | 4 ++-- lib/views/homeserver_picker.dart | 10 ++++++++-- lib/views/login.dart | 2 +- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 58b7f65..fa7f9fa 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -244,7 +244,7 @@ class MatrixState extends State { }); onJitsiCallSub ??= client.onEvent.stream .where((e) => - e.type == 'timeline' && + e.type == EventUpdateType.timeline && e.eventType == 'm.room.message' && e.content['content']['msgtype'] == Matrix.callNamespace && e.content['sender'] != client.userID) @@ -313,7 +313,7 @@ class MatrixState extends State { html.Notification.requestPermission(); onNotification ??= client.onEvent.stream .where((e) => - e.type == 'timeline' && + e.type == EventUpdateType.timeline && [EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted] .contains(e.eventType) && e.content['sender'] != client.userID) diff --git a/lib/views/homeserver_picker.dart b/lib/views/homeserver_picker.dart index a7d7b43..8d6fbcf 100644 --- a/lib/views/homeserver_picker.dart +++ b/lib/views/homeserver_picker.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -31,12 +32,17 @@ class HomeserverPicker extends StatelessWidget { } final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( - Matrix.of(context).client.checkServer(homeserver)); - if (success != false) { + checkHomeserver(homeserver, Matrix.of(context).client)); + if (success == true) { await Navigator.of(context).push(AppRoute(SignUp())); } } + Future checkHomeserver(dynamic homeserver, Client client) async { + await client.checkHomeserver(homeserver); + return true; + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/views/login.dart b/lib/views/login.dart index d7d327a..14e3e95 100644 --- a/lib/views/login.dart +++ b/lib/views/login.dart @@ -94,7 +94,7 @@ class _LoginState extends State { if ((newDomain?.isNotEmpty ?? false) && newDomain != Matrix.of(context).client.homeserver.toString()) { await SimpleDialogs(context).tryRequestWithErrorToast( - Matrix.of(context).client.checkServer(newDomain)); + Matrix.of(context).client.checkHomeserver(newDomain)); setState(() => usernameError = null); } } catch (e) { diff --git a/pubspec.lock b/pubspec.lock index 351bde9..7a1d482 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -208,8 +208,8 @@ packages: dependency: "direct main" description: path: "." - ref: be6824b7465b2bda7e5b769254be5cddd207b479 - resolved-ref: be6824b7465b2bda7e5b769254be5cddd207b479 + ref: "412da6ae0cf3aa8139a29381c4f07910d541deab" + resolved-ref: "412da6ae0cf3aa8139a29381c4f07910d541deab" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 3c2919b..1aa8c19 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: be6824b7465b2bda7e5b769254be5cddd207b479 + ref: 412da6ae0cf3aa8139a29381c4f07910d541deab localstorage: ^3.0.1+4 file_picker_cross: ^4.2.2 From 7876164dfc13a059354854f032f17b037441dcc6 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 09:59:18 +0100 Subject: [PATCH 20/32] fix: user bottom sheet design --- lib/components/user_bottom_sheet.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/components/user_bottom_sheet.dart b/lib/components/user_bottom_sheet.dart index d91c6bc..22095ef 100644 --- a/lib/components/user_bottom_sheet.dart +++ b/lib/components/user_bottom_sheet.dart @@ -153,11 +153,13 @@ class UserBottomSheet extends StatelessWidget { ), ], ), - body: ListView( + body: Column( children: [ - ContentBanner( - user.avatarUrl, - defaultIcon: Icons.person_outline, + Expanded( + child: ContentBanner( + user.avatarUrl, + defaultIcon: Icons.person_outline, + ), ), ListTile( title: Text(L10n.of(context).username), From 4981cf42959fc6b11f60b8a7d65765b465817c0d Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 28 Oct 2020 10:15:06 +0100 Subject: [PATCH 21/32] chore: Update flutter_matrix_html --- lib/components/html_message.dart | 10 +++++++++- lib/utils/famedlysdk_store.dart | 2 +- pubspec.lock | 18 ++++++++++++++++-- pubspec.yaml | 4 ++-- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/components/html_message.dart b/lib/components/html_message.dart index df508a4..a67377a 100644 --- a/lib/components/html_message.dart +++ b/lib/components/html_message.dart @@ -33,6 +33,8 @@ class HtmlMessage extends StatelessWidget { // there is no need to pre-validate the html, as we validate it while rendering + final matrix = Matrix.of(context); + final themeData = Theme.of(context); return Html( data: renderHtml, @@ -50,12 +52,18 @@ class HtmlMessage extends StatelessWidget { getMxcUrl: (String mxc, double width, double height) { final ratio = MediaQuery.of(context).devicePixelRatio; return Uri.parse(mxc)?.getThumbnail( - Matrix.of(context).client, + matrix.client, width: (width ?? 800) * ratio, height: (height ?? 800) * ratio, method: ThumbnailMethod.scale, ); }, + setCodeLanguage: (String key, String value) async { + await matrix.store.setItem('code_language.$key', value); + }, + getCodeLanguage: (String key) async { + return await matrix.store.getItem('code_language.$key'); + }, getPillInfo: (String identifier) async { if (room == null) { return null; diff --git a/lib/utils/famedlysdk_store.dart b/lib/utils/famedlysdk_store.dart index 6bb3f9c..34232e5 100644 --- a/lib/utils/famedlysdk_store.dart +++ b/lib/utils/famedlysdk_store.dart @@ -62,7 +62,7 @@ class Store { if (!PlatformInfos.isMobile) { await _setupLocalStorage(); try { - return await storage.getItem(key).toString(); + return await storage.getItem(key)?.toString(); } catch (_) { return null; } diff --git a/pubspec.lock b/pubspec.lock index 7a1d482..849b916 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -309,6 +309,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" flutter_keyboard_visibility: dependency: transitive description: @@ -348,7 +355,7 @@ packages: name: flutter_matrix_html url: "https://pub.dartlang.org" source: hosted - version: "0.1.9" + version: "0.1.10" flutter_olm: dependency: "direct main" description: @@ -408,6 +415,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + highlight: + dependency: transitive + description: + name: highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" html: dependency: transitive description: @@ -540,7 +554,7 @@ packages: name: matrix_link_text url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.2" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1aa8c19..0aa8704 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: firebase_messaging: ^7.0.2 flutter_local_notifications: ^1.4.3 # desktop_notifications: ^0.0.0-dev.4 // Currently blocked by: https://github.com/canonical/desktop_notifications.dart/issues/5 - matrix_link_text: ^0.3.1 + matrix_link_text: ^0.3.2 path_provider: ^1.5.1 downloads_path_provider_28: ^0.1.0 permission_handler: ^5.0.1+1 @@ -53,7 +53,7 @@ dependencies: open_file: ^3.0.1 mime_type: ^0.3.0 bot_toast: ^3.0.0 - flutter_matrix_html: ^0.1.9 + flutter_matrix_html: ^0.1.10 moor: ^3.3.1 sqlite3_flutter_libs: ^0.2.0 sqlite3: ^0.1.4 From b903ea90715e1a0d3a9b919ab7896f03a01243b9 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 10:56:24 +0100 Subject: [PATCH 22/32] fix: Sentry --- lib/components/matrix.dart | 4 ++-- lib/config/app_config.dart | 2 ++ lib/main.dart | 19 ++----------------- lib/utils/sentry_controller.dart | 16 ++++++++++++++++ 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index fa7f9fa..6e57617 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; import 'package:fluffychat/utils/firebase_controller.dart'; import 'package:fluffychat/utils/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/sentry_controller.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -17,7 +18,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:dbus/dbus.dart'; import 'package:desktop_notifications/desktop_notifications.dart';*/ -import '../main.dart'; import '../utils/app_route.dart'; import '../utils/beautify_string_extension.dart'; import '../utils/famedlysdk_store.dart'; @@ -98,7 +98,7 @@ class MatrixState extends State { } } catch (e, s) { client.onLoginStateChanged.sink.addError(e, s); - captureException(e, s); + SentryController.captureException(e, s); rethrow; } } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 8c50c09..a0c98ce 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -6,4 +6,6 @@ abstract class AppConfig { 'https://gitlab.com/ChristianPauly/fluffychat-flutter'; static const String supportUrl = 'https://gitlab.com/ChristianPauly/fluffychat-flutter/issues'; + static const String sentryDsn = + 'https://8591d0d863b646feb4f3dda7e5dcab38@o256755.ingest.sentry.io/5243143'; } diff --git a/lib/main.dart b/lib/main.dart index fecdff2..8f7d274 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,39 +3,24 @@ import 'dart:io'; import 'package:bot_toast/bot_toast.dart'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/utils/sentry_controller.dart'; import 'package:fluffychat/views/homeserver_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:sentry/sentry.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'components/matrix.dart'; import 'components/theme_switcher.dart'; -import 'utils/famedlysdk_store.dart'; import 'views/chat_list.dart'; -final sentry = SentryClient(dsn: '8591d0d863b646feb4f3dda7e5dcab38'); - -void captureException(error, stackTrace) async { - debugPrint(error.toString()); - debugPrint(stackTrace.toString()); - final storage = Store(); - if (await storage.getItem('sentry') == 'true') { - await sentry.captureException( - exception: error, - stackTrace: stackTrace, - ); - } -} - void main() { SystemChrome.setSystemUIOverlayStyle( SystemUiOverlayStyle(statusBarColor: Colors.transparent)); runZonedGuarded( () => runApp(App()), - captureException, + SentryController.captureException, ); } diff --git a/lib/utils/sentry_controller.dart b/lib/utils/sentry_controller.dart index 09a3a67..e4c3ed7 100644 --- a/lib/utils/sentry_controller.dart +++ b/lib/utils/sentry_controller.dart @@ -1,7 +1,9 @@ import 'package:bot_toast/bot_toast.dart'; import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:sentry/sentry.dart'; import 'famedlysdk_store.dart'; @@ -23,4 +25,18 @@ abstract class SentryController { final storage = Store(); return await storage.getItem('sentry') == 'true'; } + + static final sentry = SentryClient(dsn: AppConfig.sentryDsn); + + static void captureException(error, stackTrace) async { + debugPrint(error.toString()); + debugPrint(stackTrace.toString()); + final storage = Store(); + if (await storage.getItem('sentry') == 'true') { + await sentry.captureException( + exception: error, + stackTrace: stackTrace, + ); + } + } } From 40d00b0a5a3dba238489c994aa0085e55fbe3199 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 10:30:37 +0000 Subject: [PATCH 23/32] chore: New version --- CHANGELOG.md | 12 ++++++++++++ pubspec.yaml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd98aa1..f7fac2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# Version 0.21.0 - 2020-10-28 +### Features +- New user viewer +- Add code syntax highlighting in messages +- Updated translations: Thanks to all helpers +### Changes +- Stories feature removed +### Fixes +- Fixes sentry +- Fixes Android download +- Minor fixes + # Version 0.20.0 - 2020-10-23 ### Features - Added translations: Arabic diff --git a/pubspec.yaml b/pubspec.yaml index 0aa8704..aa87212 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Chat with your friends. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.20.0+47 +version: 0.21.0+47 environment: sdk: ">=2.6.0 <3.0.0" From 86a385dd2dae21bc21857009b69ba3f33603b4b3 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 10:41:35 +0000 Subject: [PATCH 24/32] chore: Update SDK --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 849b916..68333fa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -208,8 +208,8 @@ packages: dependency: "direct main" description: path: "." - ref: "412da6ae0cf3aa8139a29381c4f07910d541deab" - resolved-ref: "412da6ae0cf3aa8139a29381c4f07910d541deab" + ref: "955fb747c29eab76b17eb9a13ebc15026e917fb8" + resolved-ref: "955fb747c29eab76b17eb9a13ebc15026e917fb8" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index aa87212..7a66e52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: 412da6ae0cf3aa8139a29381c4f07910d541deab + ref: 955fb747c29eab76b17eb9a13ebc15026e917fb8 localstorage: ^3.0.1+4 file_picker_cross: ^4.2.2 From b471bd05aa48e758b71c9aafeceba7d7084a4b1c Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 11:54:57 +0100 Subject: [PATCH 25/32] chore: Update packages --- lib/utils/firebase_controller.dart | 22 ++++++---- pubspec.lock | 65 +++++++++++++++++------------- pubspec.yaml | 51 +++++++++++------------ 3 files changed, 77 insertions(+), 61 deletions(-) diff --git a/lib/utils/firebase_controller.dart b/lib/utils/firebase_controller.dart index 9c20f03..25b70a2 100644 --- a/lib/utils/firebase_controller.dart +++ b/lib/utils/firebase_controller.dart @@ -133,7 +133,9 @@ abstract class FirebaseController { return null; }); var initializationSettings = InitializationSettings( - initializationSettingsAndroid, initializationSettingsIOS); + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); await _flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: goToRoom); @@ -264,12 +266,14 @@ abstract class FirebaseController { ) ], ), - importance: Importance.Max, - priority: Priority.High, + importance: Importance.max, + priority: Priority.high, ticker: i18n.newMessageInFluffyChat); var iOSPlatformChannelSpecifics = IOSNotificationDetails(); var platformChannelSpecifics = NotificationDetails( - androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); await _flutterLocalNotificationsPlugin.show( 0, room.getLocalizedDisplayname(MatrixLocals(i18n)), @@ -299,7 +303,9 @@ abstract class FirebaseController { AndroidInitializationSettings('notifications_icon'); var initializationSettingsIOS = IOSInitializationSettings(); var initializationSettings = InitializationSettings( - initializationSettingsAndroid, initializationSettingsIOS); + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); await flutterLocalNotificationsPlugin.initialize(initializationSettings); // FIXME unable to init without context currently https://github.com/flutter/flutter/issues/67092 @@ -322,10 +328,12 @@ abstract class FirebaseController { // Display notification var androidPlatformChannelSpecifics = AndroidNotificationDetails( CHANNEL_ID, CHANNEL_NAME, CHANNEL_DESCRIPTION, - importance: Importance.Max, priority: Priority.High); + importance: Importance.max, priority: Priority.high); var iOSPlatformChannelSpecifics = IOSNotificationDetails(); var platformChannelSpecifics = NotificationDetails( - androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); final title = l10n.unreadChats(unread.toString()); await flutterLocalNotificationsPlugin.show( 1, title, l10n.openAppToReadMessages, platformChannelSpecifics, diff --git a/pubspec.lock b/pubspec.lock index 68333fa..e339638 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -168,7 +168,14 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "1.0.0" + dapackages: + dependency: "direct dev" + description: + name: dapackages + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" dart_style: dependency: transitive description: @@ -233,28 +240,21 @@ packages: name: file_chooser url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.6" file_picker: dependency: transitive description: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "1.13.3" + version: "2.0.11" file_picker_cross: dependency: "direct main" description: name: file_picker_cross url: "https://pub.dartlang.org" source: hosted - version: "4.2.2" - file_picker_platform_interface: - dependency: transitive - description: - name: file_picker_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "4.2.6" firebase: dependency: transitive description: @@ -268,7 +268,7 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "0.5.0" + version: "0.5.0+1" firebase_core_platform_interface: dependency: transitive description: @@ -289,7 +289,7 @@ packages: name: firebase_messaging url: "https://pub.dartlang.org" source: hosted - version: "7.0.2" + version: "7.0.3" flutter: dependency: "direct main" description: flutter @@ -336,14 +336,14 @@ packages: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "1.4.4+4" + version: "3.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -470,7 +470,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.6.7+11" + version: "0.6.7+12" image_picker_platform_interface: dependency: transitive description: @@ -519,7 +519,7 @@ packages: name: localstorage url: "https://pub.dartlang.org" source: hosted - version: "3.0.2+5" + version: "3.0.3+6" logging: dependency: transitive description: @@ -582,7 +582,7 @@ packages: name: moor url: "https://pub.dartlang.org" source: hosted - version: "3.3.1" + version: "3.4.0" native_imaging: dependency: "direct main" description: @@ -633,7 +633,7 @@ packages: name: open_file url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.3" package_config: dependency: transitive description: @@ -668,7 +668,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.18" + version: "1.6.22" path_provider_linux: dependency: transitive description: @@ -822,7 +822,7 @@ packages: name: share url: "https://pub.dartlang.org" source: hosted - version: "0.6.5+2" + version: "0.6.5+4" shelf: dependency: transitive description: @@ -883,7 +883,7 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "1.3.1+1" + version: "1.3.2" sqflite_common: dependency: transitive description: @@ -897,7 +897,7 @@ packages: name: sqlite3 url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.7" sqlite3_flutter_libs: dependency: "direct main" description: @@ -968,6 +968,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.12-nullsafety.5" + timezone: + dependency: transitive + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.9" typed_data: dependency: transitive description: @@ -1002,7 +1009,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.7.2" + version: "5.7.8" url_launcher_linux: dependency: transitive description: @@ -1023,14 +1030,14 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.8" + version: "1.0.9" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.4+1" + version: "0.1.5" url_launcher_windows: dependency: transitive description: @@ -1093,7 +1100,7 @@ packages: name: webview_flutter url: "https://pub.dartlang.org" source: hosted - version: "0.3.24" + version: "1.0.5" win32: dependency: transitive description: @@ -1130,5 +1137,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.10.0-110 <2.11.0" - flutter: ">=1.20.0 <2.0.0" + dart: ">=2.10.2 <2.11.0" + flutter: ">=1.22.2 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7a66e52..0b91124 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,50 +22,50 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 + cupertino_icons: ^1.0.0 famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git ref: 955fb747c29eab76b17eb9a13ebc15026e917fb8 - localstorage: ^3.0.1+4 - file_picker_cross: ^4.2.2 - image_picker: ^0.6.7+11 - url_launcher: ^5.7.2 + localstorage: ^3.0.3+6 + file_picker_cross: ^4.2.6 + image_picker: ^0.6.7+12 + url_launcher: ^5.7.8 cached_network_image: ^2.3.3 - firebase_messaging: ^7.0.2 - flutter_local_notifications: ^1.4.3 + firebase_messaging: ^7.0.3 + flutter_local_notifications: ^3.0.1 # desktop_notifications: ^0.0.0-dev.4 // Currently blocked by: https://github.com/canonical/desktop_notifications.dart/issues/5 matrix_link_text: ^0.3.2 - path_provider: ^1.5.1 + path_provider: ^1.6.22 downloads_path_provider_28: ^0.1.0 permission_handler: ^5.0.1+1 - webview_flutter: ^0.3.19+9 - share: ^0.6.3+5 + webview_flutter: ^1.0.5 + share: ^0.6.5+4 flutter_secure_storage: ^3.3.5 - http: ^0.12.0+4 - universal_html: ^1.1.12 - receive_sharing_intent: ^1.3.3 - flutter_slidable: ^0.5.4 + http: ^0.12.2 + universal_html: ^1.2.3 + receive_sharing_intent: ^1.4.1 + flutter_slidable: ^0.5.7 photo_view: ^0.10.2 - flutter_sound: ^2.1.1 - open_file: ^3.0.1 - mime_type: ^0.3.0 - bot_toast: ^3.0.0 + flutter_sound: 2.1.1 + open_file: ^3.0.3 + mime_type: ^0.3.2 + bot_toast: ^3.0.4 flutter_matrix_html: ^0.1.10 - moor: ^3.3.1 + moor: ^3.4.0 sqlite3_flutter_libs: ^0.2.0 - sqlite3: ^0.1.4 - random_string: ^2.0.1 - flutter_typeahead: ^1.8.1 + sqlite3: ^0.1.7 + random_string: ^2.1.0 + flutter_typeahead: ^1.8.8 flutter_olm: ^1.0.1 intl: ^0.16.1 - intl_translation: ^0.17.9 + intl_translation: ^0.17.10+1 circular_check_box: ^1.0.4 flutter_localizations: sdk: flutter - sqflite: ^1.1.7 # Still used to obtain the database location + sqflite: ^1.3.2 # Still used to obtain the database location native_imaging: git: url: https://gitlab.com/famedly/libraries/native_imaging.git @@ -80,7 +80,8 @@ dev_dependencies: sdk: flutter flutter_launcher_icons: "^0.7.4" - pedantic: ^1.9.0 + pedantic: ^1.9.2 + dapackages: ^1.3.0 flutter_icons: android: "launcher_icon" From aa191c197acdafa39c51fb9dda93ee8d996ddf69 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 12:17:23 +0100 Subject: [PATCH 26/32] fix: File picker issue --- pubspec.lock | 11 +++++++++-- pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e339638..b747413 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -247,14 +247,21 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "1.13.3" file_picker_cross: dependency: "direct main" description: name: file_picker_cross url: "https://pub.dartlang.org" source: hosted - version: "4.2.6" + version: "4.2.2" + file_picker_platform_interface: + dependency: transitive + description: + name: file_picker_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" firebase: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0b91124..40a3933 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: ref: 955fb747c29eab76b17eb9a13ebc15026e917fb8 localstorage: ^3.0.3+6 - file_picker_cross: ^4.2.6 + file_picker_cross: 4.2.2 image_picker: ^0.6.7+12 url_launcher: ^5.7.8 cached_network_image: ^2.3.3 From c3e23b6e6a3d4879f6a237ebec62747954974088 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 12:41:19 +0100 Subject: [PATCH 27/32] fix: Target sdk --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 8ff431e..d1d5523 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -45,7 +45,7 @@ android { defaultConfig { applicationId "chat.fluffy.fluffychat" minSdkVersion 21 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From bcf75fc69da65ecac6f631c7537b61d8ff7bd371 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 12:42:59 +0100 Subject: [PATCH 28/32] fix: CompileSDKVersion --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d1d5523..71589f5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 28 + compileSdkVersion 29 sourceSets { main.java.srcDirs += 'src/main/kotlin' From f93f9c28ecef926d94783a9ebfb611bbf7d6aa12 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 28 Oct 2020 12:54:58 +0100 Subject: [PATCH 29/32] chore: Change compileSdkVersion again --- android/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 71589f5..9b21168 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 29 + compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -45,7 +45,7 @@ android { defaultConfig { applicationId "chat.fluffy.fluffychat" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From d1dfa9c8cf568ea80ab3f3c5462be8ba2bfb03ad Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 28 Oct 2020 13:45:42 +0100 Subject: [PATCH 30/32] chore: update version code --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 40a3933..98d6aaf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Chat with your friends. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.21.0+47 +version: 0.21.1+48 environment: sdk: ">=2.6.0 <3.0.0" From db2b357f478d6ca0d39a3a50454641f74cbf3a21 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 28 Oct 2020 15:01:16 +0100 Subject: [PATCH 31/32] fix user popup sometimes crashing --- lib/utils/presence_extension.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/presence_extension.dart b/lib/utils/presence_extension.dart index 22ea01b..50203f8 100644 --- a/lib/utils/presence_extension.dart +++ b/lib/utils/presence_extension.dart @@ -32,7 +32,7 @@ extension PresenceExtension on Presence { if (presence.statusMsg?.isNotEmpty ?? false) { return presence.statusMsg; } - if (presence.currentlyActive) { + if (presence.currentlyActive ?? false) { return L10n.of(context).currentlyActive; } return presence.presence.getLocalized(context); From fab1e1b980f9790e07a79a5dcc12167896a0c890 Mon Sep 17 00:00:00 2001 From: Tirifto Date: Wed, 28 Oct 2020 20:09:00 +0100 Subject: [PATCH 32/32] Added translation using Weblate (Esperanto) --- lib/l10n/intl_eo.arb | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/l10n/intl_eo.arb diff --git a/lib/l10n/intl_eo.arb b/lib/l10n/intl_eo.arb new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/lib/l10n/intl_eo.arb @@ -0,0 +1 @@ +{}