diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 1dbf1aed6..24ffbcb55 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "2.0.6", + "flutterSdkVersion": "2.2.2", "flavors": {} } \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f20a6c9b0..68ef4358b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,17 +1,18 @@ -## Changes +### Types +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Refactoring (fix or feature that would cause existing functionality to not work as expected) +- [ ] Breaking change (non-breaking change which improves code quality - QA thoroughly) -### 🔮 Features -### 🔒 Security -### 🛠 Performance -### 🐛 Fixes -### 📐 Refactoring +### Changes -### Media (if applicable) +#### 🔮 Features +#### 🔒 Security +#### 🛠 Performance +#### 🐛 Fixes +#### 📐 Refactoring -### Types of changes -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +### Media (if applicable) ### QA diff --git a/analysis_options.yaml b/analysis_options.yaml index 99d861773..f42d14106 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -16,6 +16,7 @@ linter: avoid_escaping_inner_quotes: false prefer_for_elements_to_map_fromIterable: false # that syntax is challenging, will change if becomes standard prefer_conditional_assignment: false + sized_box_for_whitespace: false # Enabled prefer_single_quotes: true diff --git a/assets/translations/en.json b/assets/translations/en.json index 0e1acf6fb..eb138a8a2 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -16,7 +16,7 @@ "title-view-invite": "Invite", "title-view-preferences-chat": "Chats", "title-view-privacy": "Privacy", - "title-view-homeserver-search": "Find Your Homeserve", + "title-view-homeserver-search": "Find Your Homeserver", "title-view-profile": "Set up Your Profile", "title-dialog-delete-keys": "Confirm Deleting Keys", "title-dialog-encryption": "Encrypt Chat?", @@ -25,6 +25,7 @@ "title-dialog-terms-alpha": "Confirm Open Alpha Terms Of Service", "title-dialog-email-requirement": "Email requirement", "title-dialog-email-requirement-verified": "Email verification", + "title-confirm-password": "Confirm Password", "label-search-homeservers": "Search for homeservers...", "label-search-users": "Search for users...", "label-search-users-results": "Matched Users", @@ -58,11 +59,12 @@ "list-item-settings-sms": "SMS and MMS", "list-item-settings-logout": "Logout", "alert-restart-app-effect": "You'll need to close and restart the app for this to take affect", - "alert-invite-user-unknown": "This user doens't appear to exist within matrix, but you can attempt to invite them anyway.\n\nMake sure you have the correct name before trying.", + "alert-invite-user-unknown": "This user doesn't appear to exist within matrix, but you can attempt to invite them anyway.\n\nMake sure you have the correct name before trying.", "alert-feature-in-progress": "🛠 This feature is coming soon", "alert-message-failed": "Message Failed To Send", "alert-homeserver-invalid": "This server failed the 'well-known' check, make sure the server is configured correctly", - "prompt-homeserver-select": "Select you username homeserver", + "prompt-homeserver-select": "Select your username and homeserver", + "prompt-confirm-deactivate": "Enter your password below to confirm deactivation", "content-intro-section-one": "{} works by using an encrypted \nand decentralized protocol \ncalled ", "content-intro-section-two": "Matrix enables you to message others", "content-intro-section-two-part-two": "\nprivately and control ", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 7745f4eb3..c2c7ddad9 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -25,6 +25,7 @@ "title-dialog-terms-alpha": "Подтвердить Условия пользования Открытой Альфы", "title-dialog-email-requirement": "Email требуется", "title-dialog-email-requirement-verified": "Email верификация", + "title-confirm-password": "подтвердите пароль", "label-search-homeservers": "Поиск домашних серверов...", "label-search-users": "Поиск пользователей...", "label-search-users-results": "Найденные Пользователи", @@ -50,11 +51,12 @@ "button-text-new-user": "Не имеешь юзернейм?", "button-text-login": "Войти", "button-text-signup": "Create One", - "alert-invite-user-unknown": "This user doens't appear to exist within matrix, but you can attempt to invite them anyway.\n\nMake sure you have the correct name before trying.", + "alert-invite-user-unknown": "This user doesn't appear to exist within matrix, but you can attempt to invite them anyway.\n\nMake sure you have the correct name before trying.", "alert-feature-in-progress": "🛠 Эта функция будет скоро", "alert-message-failed": "Message Failed To Send", "alert-homeserver-invalid": "This server failed the 'well-known' check, make sure the server is configured correctly", - "prompt-homeserver-select": "Select you username homeserver", + "prompt-homeserver-select": "Select your username and homeserver", + "prompt-confirm-deactivate": "Enter your password below to confirm deactivation", "content-intro-section-one": "{} works by using an encrypted \nand decentralized protocol \ncalled ", "content-intro-section-two": "Matrix enables you to message others", "content-intro-section-two-part-two": "\nprivately and control ", diff --git a/lib/cache/codec.dart b/lib/cache/codec.dart index 413d26faa..a3586d9f6 100644 --- a/lib/cache/codec.dart +++ b/lib/cache/codec.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:encrypt/encrypt.dart'; const IV_LENGTH = 16; -const IV_LENGTH_BASE_64 = (IV_LENGTH + (IV_LENGTH / 2)); +const IV_LENGTH_BASE_64 = IV_LENGTH + (IV_LENGTH / 2); /// Random bytes generator Uint8List _generateIV(int length) { diff --git a/lib/cache/middleware.dart b/lib/cache/middleware.dart new file mode 100644 index 000000000..0ec4da5af --- /dev/null +++ b/lib/cache/middleware.dart @@ -0,0 +1,29 @@ +import 'package:redux/redux.dart'; +import 'package:syphon/global/print.dart'; +import 'package:syphon/store/auth/actions.dart'; +import 'package:syphon/store/crypto/actions.dart'; +import 'package:syphon/store/index.dart'; +import 'package:syphon/store/rooms/actions.dart'; + +/// +/// Cache Middleware +/// +/// Saves store data to cold storage based +/// on which redux actions are fired. +/// +bool cacheMiddleware(Store store, dynamic action) { + switch (action.runtimeType) { + case SetRoom: + case RemoveRoom: + case SetOlmAccount: + case SetOlmAccountBackup: + case SetDeviceKeysOwned: + case SetUser: + case ResetCrypto: + case ResetUser: + printInfo('[initStore] persistor saving from ${action.runtimeType}'); + return true; + default: + return false; + } +} diff --git a/lib/cache/serializer.dart b/lib/cache/serializer.dart index 9fb63e1bf..5374fe348 100644 --- a/lib/cache/serializer.dart +++ b/lib/cache/serializer.dart @@ -1,18 +1,14 @@ -// Dart imports: import 'dart:convert'; import 'dart:typed_data'; -// Flutter imports: import 'package:flutter/foundation.dart'; -// Package imports: import 'package:redux_persist/redux_persist.dart'; import 'package:sembast/sembast.dart'; import 'package:syphon/cache/index.dart'; import 'package:syphon/cache/threadables.dart'; import 'package:syphon/global/print.dart'; -// Project imports: import 'package:syphon/store/crypto/state.dart'; import 'package:syphon/store/events/ephemeral/m.read/model.dart'; import 'package:syphon/store/events/messages/model.dart'; diff --git a/lib/cache/threadables.dart b/lib/cache/threadables.dart index f054f26ec..7fd146c9b 100644 --- a/lib/cache/threadables.dart +++ b/lib/cache/threadables.dart @@ -1,9 +1,7 @@ import 'dart:convert'; import 'dart:async'; -import 'package:encrypt/encrypt.dart'; import 'package:syphon/cache/codec.dart'; -import 'package:syphon/global/print.dart'; Future encryptJsonBackground(Map params) async { String json = params['json']; diff --git a/lib/global/algos.dart b/lib/global/algos.dart index 6679cc0ae..12bd41c25 100644 --- a/lib/global/algos.dart +++ b/lib/global/algos.dart @@ -1,7 +1,3 @@ -// Dart imports: -import 'dart:convert'; - -import 'package:flutter/material.dart'; import 'package:syphon/global/print.dart'; List fibonacci(int n) { diff --git a/lib/global/colours.dart b/lib/global/colours.dart index 9baf5679d..c96014749 100644 --- a/lib/global/colours.dart +++ b/lib/global/colours.dart @@ -1,4 +1,3 @@ -// Flutter imports: import 'package:flutter/material.dart'; /// British localization because @@ -11,12 +10,15 @@ class Colours { static const greyEnabled = 0xffFAFAFA; static const greyDisabled = 0xffD8D8D8; - static const greyDark = 0xff4D5767; - static const greyBubble = 0xffEEEEEE; - static const blackDefault = 0xff121212; + static const greyDefault = 0xFF9E9E9E; // Colors.grey[500] + static const greyLight = 0xFFE0E0E0; // Colors.grey[300] + static const greyLightest = 0xFFEEEEEE; // Colors.grey[200] + static const greyDark = 0xFF616161; // Colors.grey[700] + static const greyDarkest = 0xFF303030; // Colors.grey[850] + static const blackFull = 0xff000000; - static const greyDefault = 0xFF9E9E9E; + static const blackDefault = 0xff121212; static const whiteDefault = 0xffFEFEFE; // Material colors at shades of 700 diff --git a/lib/global/dimensions.dart b/lib/global/dimensions.dart index 566ea44d4..af8a6d8df 100644 --- a/lib/global/dimensions.dart +++ b/lib/global/dimensions.dart @@ -1,4 +1,3 @@ -// Flutter imports: import 'package:flutter/material.dart'; class Dimensions { diff --git a/lib/global/formatters.dart b/lib/global/formatters.dart index 77bde8278..d8343e405 100644 --- a/lib/global/formatters.dart +++ b/lib/global/formatters.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:intl/intl.dart'; // @again_guy:matrix.org -> again_ereio @@ -21,12 +20,41 @@ String formatLanguageCode(String? language) { } } -// @again_guy:matrix.org -> ER -String formatSenderInitials(String sender) { - final formattedSender = formatSender(sender).toUpperCase(); - return formattedSender.length < 2 - ? formattedSender - : formattedSender.substring(0, 2); +// @again_guy:matrix.org -> AG +// a -> A +String formatInitials(String? word) { + final wordUppercase = (word ?? '').toUpperCase(); + return wordUppercase.length > 1 ? wordUppercase.substring(0, 2) : wordUppercase; +} + +String formatInitialsLong(String? fullword) { + // -> ? + if (fullword == null || fullword.isEmpty) { + return '?'; + } + + final word = fullword.replaceAll('@', ''); + + if (word.isEmpty) { + return '?'; + } + + // example words -> EW + if (word.length > 2 && word.contains(' ') && word.split(' ')[1].isNotEmpty) { + final words = word.split(' '); + final wordOne = words.elementAt(0); + final wordTwo = words.elementAt(1); + + var initials = ''; + initials = wordOne.isEmpty ? initials : initials + wordOne.substring(0, 1); + initials = wordTwo.isEmpty ? initials : initials + wordTwo.substring(0, 1); + + return initials.toUpperCase(); + } + + final initials = word.length > 1 ? word.substring(0, 2) : word.substring(0, 1); + + return initials.toUpperCase(); } String formatTimestampFull({ diff --git a/lib/global/libs/https.dart b/lib/global/libs/https.dart index c13253a37..9f81bc217 100644 --- a/lib/global/libs/https.dart +++ b/lib/global/libs/https.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:http/http.dart' as http; var httpClient; diff --git a/lib/global/libs/jack/index.dart b/lib/global/libs/jack/index.dart index 18eb5d69d..5fde79144 100644 --- a/lib/global/libs/jack/index.dart +++ b/lib/global/libs/jack/index.dart @@ -3,19 +3,20 @@ import 'dart:convert'; import 'package:http/http.dart' as http; /// Jack API -/// +/// /// Eventually will be a library of non-authenticated /// functions that will search or scrape for matrix /// servers for Syphon -/// +/// class JackApi { static const List endpoints = [ 'https://www.hello-matrix.net/public_servers.php?format=json&only_public=true&show_from=United+States+(Denver)', ]; + /// Fetch Public Homeservers (hello matrix) - /// + /// /// Returns an array of homeseerver objects - static Future fetchPublicServers() async { + static Future> fetchPublicServers() async { final String url = endpoints.elementAt(0); final response = await http.get(Uri.parse(url)); diff --git a/lib/global/libs/matrix/auth.dart b/lib/global/libs/matrix/auth.dart index 1788e62f8..09f96964c 100644 --- a/lib/global/libs/matrix/auth.dart +++ b/lib/global/libs/matrix/auth.dart @@ -1,16 +1,15 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; import 'dart:math'; -// Package imports: import 'package:http/http.dart' as http; +import 'package:syphon/global/print.dart'; import 'package:syphon/global/values.dart'; /// https://matrix.org/docs/spec/client_server/latest#id183 -/// +/// /// Authentication Types -/// +/// /// Can be used during actual login or interactive auth for confirmation class MatrixAuthTypes { static const PASSWORD = 'm.login.password'; @@ -72,7 +71,7 @@ abstract class Auth { final response = await http.post( Uri.parse(url), - headers: {'Content-type': 'application/json'}, + headers: {...Values.defaultHeaders}, body: json.encode(body), ); @@ -116,7 +115,7 @@ abstract class Auth { final response = await http.post( Uri.parse(url), - headers: {'Content-type': 'application/json'}, + headers: {...Values.defaultHeaders}, body: json.encode(body), ); @@ -124,8 +123,8 @@ abstract class Auth { } /// Register New User - /// - /// inhibit_login automatically logs in the user after creation + /// + /// inhibit_login automatically logs in the user after creation static Future registerEmail({ String? protocol, String? homeserver, @@ -133,8 +132,7 @@ abstract class Auth { String? email, int? sendAttempt = 1, }) async { - final String url = - '$protocol$homeserver/_matrix/client/r0/register/email/requestToken'; + final String url = '$protocol$homeserver/_matrix/client/r0/register/email/requestToken'; final Map body = { 'email': email, @@ -144,7 +142,7 @@ abstract class Auth { final response = await http.post( Uri.parse(url), - headers: {'Content-type': 'application/json'}, + headers: {...Values.defaultHeaders}, body: json.encode(body), ); @@ -152,8 +150,8 @@ abstract class Auth { } /// Register New User - /// - /// inhibit_login automatically logs in the user after creation + /// + /// inhibit_login automatically logs in the user after creation static Future registerUser({ String? protocol, String? homeserver, @@ -225,7 +223,7 @@ abstract class Auth { final response = await http.post( Uri.parse(url), - headers: {'Content-type': 'application/json'}, + headers: {...Values.defaultHeaders}, body: json.encode(body), ); @@ -273,10 +271,10 @@ abstract class Auth { return await json.decode(response.body); } - /// https://matrix.org/docs/spec/client_server/latest#id211 - /// + /// https://matrix.org/docs/spec/client_server/latest#id211 + /// /// Check Username Availability - /// + /// /// Used to check what types of logins are available on the server static Future checkUsernameAvailability({ String? protocol = 'https://', @@ -292,10 +290,10 @@ abstract class Auth { return await json.decode(response.body); } - /// https://matrix.org/docs/spec/client_server/latest#id211 - /// + /// https://matrix.org/docs/spec/client_server/latest#id211 + /// /// Check Username Availability - /// + /// /// Used to check what types of logins are available on the server static Future checkHomeserver({ String protocol = 'https://', @@ -320,9 +318,9 @@ abstract class Auth { } /// Update User Password - /// + /// /// https://matrix.org/docs/spec/client_server/latest#id198 - /// + /// static Future updatePassword({ String? protocol, String? homeserver, @@ -396,7 +394,7 @@ abstract class Auth { final response = await http.post( Uri.parse(url), - headers: {'Content-type': 'application/json'}, + headers: {...Values.defaultHeaders}, body: json.encode(body), ); @@ -415,8 +413,7 @@ abstract class Auth { String? email, int sendAttempt = 1, }) async { - final String url = - '$protocol$homeserver/_matrix/client/r0/account/password/email/requestToken'; + final String url = '$protocol$homeserver/_matrix/client/r0/account/password/email/requestToken'; final Map body = { 'email': email, @@ -426,7 +423,7 @@ abstract class Auth { final response = await http.post( Uri.parse(url), - headers: {'Content-type': 'application/json'}, + headers: {...Values.defaultHeaders}, body: json.encode(body), ); diff --git a/lib/global/libs/matrix/devices.dart b/lib/global/libs/matrix/devices.dart index 9365bb5c2..9f9985ac0 100644 --- a/lib/global/libs/matrix/devices.dart +++ b/lib/global/libs/matrix/devices.dart @@ -1,14 +1,12 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; -// Package imports: import 'package:http/http.dart' as http; import 'package:syphon/global/values.dart'; abstract class Devices { /// https://matrix.org/docs/spec/client_server/latest#id472 - /// + /// /// HTTP:GET /// Gets all currently active pushers for the authenticated user. static Future fetchDevices({ @@ -28,7 +26,7 @@ abstract class Devices { } /// https://matrix.org/docs/spec/client_server/latest#id412 - /// + /// /// HTTP:PUT /// Gets all currently active pushers for the authenticated user. static Future updateDevice({ @@ -73,7 +71,8 @@ abstract class Devices { String? authType, String? authValue, }) async { - final String url = '$protocol$homeserver/_matrix/client/r0/devices/$deviceId'; + final String url = + '$protocol$homeserver/_matrix/client/r0/devices/$deviceId'; final Map headers = { 'Authorization': 'Bearer $accessToken', diff --git a/lib/global/libs/matrix/encryption.dart b/lib/global/libs/matrix/encryption.dart index ae90eb7c0..cc8c47460 100644 --- a/lib/global/libs/matrix/encryption.dart +++ b/lib/global/libs/matrix/encryption.dart @@ -1,10 +1,7 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; -// Package imports: import 'package:http/http.dart' as http; -import 'package:syphon/global/algos.dart'; import 'package:syphon/global/libs/matrix/constants.dart'; import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/global/values.dart'; diff --git a/lib/global/libs/matrix/errors.dart b/lib/global/libs/matrix/errors.dart index a0dbef43d..1c4c9a00f 100644 --- a/lib/global/libs/matrix/errors.dart +++ b/lib/global/libs/matrix/errors.dart @@ -5,6 +5,7 @@ class MatrixErrors { static const String user_in_use = 'M_USER_IN_USE'; static const String unknown_token = 'M_UNKNOWN_TOKEN'; static const String email_in_use = 'M_THREEPID_IN_USE'; + static const String weak_password = 'M_WEAK_PASSWORD'; } class MatrixErrorsSoft { diff --git a/lib/global/libs/matrix/events.dart b/lib/global/libs/matrix/events.dart index 3ff960105..53e32a1d2 100644 --- a/lib/global/libs/matrix/events.dart +++ b/lib/global/libs/matrix/events.dart @@ -1,21 +1,17 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; -// Package imports: import 'package:http/http.dart' as http; -// Project imports: import 'package:syphon/global/libs/matrix/encryption.dart'; import 'package:syphon/global/values.dart'; -import 'package:syphon/store/events/model.dart'; import 'package:syphon/global/libs/matrix/constants.dart'; abstract class Events { /// Fetch State Events - /// + /// /// https://matrix.org/docs/spec/client_server/latest#id258 - /// + /// /// Get the state events for the current state of a room. static Future fetchStateEvents({ String? protocol = 'https://', @@ -23,7 +19,8 @@ abstract class Events { String? accessToken, String? roomId, }) async { - final String url = '$protocol$homeserver/_matrix/client/r0/rooms/$roomId/state'; + final String url = + '$protocol$homeserver/_matrix/client/r0/rooms/$roomId/state'; final Map headers = { 'Authorization': 'Bearer $accessToken', @@ -73,8 +70,8 @@ abstract class Events { } /// Sync (Background Isolate) (main functionality) - /// - /// https://matrix.org/docs/spec/client_server/latest#id251 + /// + /// https://matrix.org/docs/spec/client_server/latest#id251 static Future fetchMessageEventsMapped(Map params) async { return await fetchMessageEvents( protocol: params['protocol'], @@ -89,13 +86,13 @@ abstract class Events { } /// Send Encrypted Message - /// + /// /// https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid - /// + /// /// Notes on requestId (considered a transactionId in Matrix) - /// - /// The transaction ID for this event. - /// Clients should generate an ID unique across requests with the same access token; + /// + /// The transaction ID for this event. + /// Clients should generate an ID unique across requests with the same access token; /// it will be used by the server to ensure idempotency of requests. <- really a requestId static Future sendMessageEncrypted({ @@ -140,13 +137,13 @@ abstract class Events { } /// Send Event (State Only) - /// + /// /// https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid - /// + /// /// Notes on requestId (considered a transactionId in Matrix) - /// - /// The transaction ID for this event. - /// Clients should generate an ID unique across requests with the same access token; + /// + /// The transaction ID for this event. + /// Clients should generate an ID unique across requests with the same access token; /// it will be used by the server to ensure idempotency of requests. <- really a requestId static Future sendEvent({ String? protocol = 'https://', @@ -177,13 +174,13 @@ abstract class Events { } /// Send Message - /// + /// /// https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid - /// + /// /// Notes on requestId (considered a transactionId in Matrix) - /// - /// The transaction ID for this event. - /// Clients should generate an ID unique across requests with the same access token; + /// + /// The transaction ID for this event. + /// Clients should generate an ID unique across requests with the same access token; /// it will be used by the server to ensure idempotency of requests. <- really a requestId static Future sendMessage({ String? protocol = 'https://', @@ -228,13 +225,13 @@ abstract class Events { } /// Send Reaction - /// + /// /// https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid - /// + /// /// Notes on requestId (considered a transactionId in Matrix) - /// - /// The transaction ID for this event. - /// Clients should generate an ID unique across requests with the same access token; + /// + /// The transaction ID for this event. + /// Clients should generate an ID unique across requests with the same access token; /// it will be used by the server to ensure idempotency of requests. <- really a requestId static Future sendReaction({ String protocol = 'https://', @@ -303,13 +300,13 @@ abstract class Events { } /// Send (Event) To Device - /// + /// /// https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-sendtodevice-eventtype-txnid - /// + /// /// Not intended for messages per protocol requirements - /// + /// /// This endpoint is used to send send-to-device events to a set of client devices. - /// The messages to send. A map from user ID, to a map from device ID to message body. + /// The messages to send. A map from user ID, to a map from device ID to message body. /// The device ID may also be *, meaning all known devices for the user. static Future sendEventToDevice({ String? protocol = 'https://', @@ -343,7 +340,7 @@ abstract class Events { return await json.decode(response.body); } - /// Send Typing Event + /// Send Typing Event static Future sendTyping({ String? protocol = 'https://', String? homeserver = 'matrix.org', @@ -375,7 +372,7 @@ abstract class Events { } /// Send Read Receipts - /// + /// /// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-receipt-receipttype-eventid static Future sendReadMarkers({ String? protocol = 'https://', diff --git a/lib/global/libs/matrix/index.dart b/lib/global/libs/matrix/index.dart index 6713e35f7..1f2959bf3 100644 --- a/lib/global/libs/matrix/index.dart +++ b/lib/global/libs/matrix/index.dart @@ -61,6 +61,7 @@ abstract class MatrixApi { static const saveAccountData = Users.saveAccountData; static const updateDisplayName = Users.updateDisplayName; static const updateAvatarUri = Users.updateAvatarUri; + static const deactivateUser = Users.deactivateUser; // Users static const inviteUser = Users.inviteUser; diff --git a/lib/global/libs/matrix/media.dart b/lib/global/libs/matrix/media.dart index ff4690f64..36ac10df2 100644 --- a/lib/global/libs/matrix/media.dart +++ b/lib/global/libs/matrix/media.dart @@ -1,12 +1,10 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; -// Package imports: import 'package:http/http.dart' as http; /// Media queries for matrix -/// +/// /// Testing out using a "params map" /// as the default to allow calling from /// a non-ui thread @@ -42,7 +40,8 @@ class Media { // Parce the mxc uri for the server location and id final String mediaId = mediaUriParts[mediaUriParts.length - 1]; - final String mediaServer = serverName ?? mediaUriParts[mediaUriParts.length - 2]; + final String mediaServer = + serverName ?? mediaUriParts[mediaUriParts.length - 2]; String url = '$protocol$homeserver/_matrix/media/r0/thumbnail/$mediaServer/$mediaId'; @@ -130,7 +129,7 @@ dynamic buildMediaDownloadRequest({ } /// https://matrix.org/docs/spec/client_server/latest#id392 -/// +/// /// Upload some content to the content repository. dynamic buildMediaUploadRequest({ String protocol = 'https://', diff --git a/lib/global/libs/matrix/notifications.dart b/lib/global/libs/matrix/notifications.dart index 5e153eeb0..6e99f88bf 100644 --- a/lib/global/libs/matrix/notifications.dart +++ b/lib/global/libs/matrix/notifications.dart @@ -1,16 +1,14 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; -// Package imports: import 'package:http/http.dart' as http; import 'package:syphon/global/values.dart'; abstract class Notifications { /// Fetch Notification Pushers - /// + /// /// https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers - /// + /// /// Gets all currently active pushers for the authenticated user. static Future fetchNotifications({ String? protocol = 'https://', @@ -40,9 +38,9 @@ abstract class Notifications { } /// Fetch Notification Pushers - /// + /// /// https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers - /// + /// /// Gets all currently active pushers for the authenticated user. static Future fetchNotificationPushers({ String? protocol = 'https://', @@ -81,11 +79,11 @@ abstract class Notifications { } */ /// Save Notification Pusher - /// + /// /// https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers - /// - /// This endpoint allows the creation, modification and deletion of pushers for - /// this user ID. The behaviour of this endpoint varies depending on the values + /// + /// This endpoint allows the creation, modification and deletion of pushers for + /// this user ID. The behaviour of this endpoint varies depending on the values /// in the JSON body. static Future saveNotificationPusher({ String? protocol = 'https://', diff --git a/lib/global/libs/matrix/rooms.dart b/lib/global/libs/matrix/rooms.dart index f489ef307..ec0810958 100644 --- a/lib/global/libs/matrix/rooms.dart +++ b/lib/global/libs/matrix/rooms.dart @@ -1,8 +1,6 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; -// Package imports: import 'package:http/http.dart' as http; import 'package:syphon/global/values.dart'; diff --git a/lib/global/libs/matrix/search.dart b/lib/global/libs/matrix/search.dart index 0e06ca4e3..c6b51fd76 100644 --- a/lib/global/libs/matrix/search.dart +++ b/lib/global/libs/matrix/search.dart @@ -1,17 +1,15 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; -// Package imports: import 'package:http/http.dart' as http; import 'package:syphon/global/values.dart'; /// https://matrix.org/docs/spec/client_server/latest#id295 /// 10.5.3 GET /_matrix/client/r0/publicRooms -/// -/// Lists the public rooms on the server. This API returns paginated responses. +/// +/// Lists the public rooms on the server. This API returns paginated responses. /// The rooms are ordered by the number of joined members, with the largest rooms first. -/// +/// /// Response { /// 'chunk' : [], /// 'next_batch': XXX @@ -26,7 +24,8 @@ class Search { String? searchText, String? since, }) async { - final String url = '$protocol$homeserver/_matrix/client/r0/user_directory/search'; + final String url = + '$protocol$homeserver/_matrix/client/r0/user_directory/search'; final Map headers = { 'Authorization': 'Bearer $accessToken', ...Values.defaultHeaders, diff --git a/lib/global/libs/matrix/user.dart b/lib/global/libs/matrix/user.dart index 23becff43..1fba2cb5c 100644 --- a/lib/global/libs/matrix/user.dart +++ b/lib/global/libs/matrix/user.dart @@ -1,21 +1,19 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; -// Package imports: import 'package:http/http.dart' as http; -// Project imports: import 'package:syphon/global/libs/matrix/constants.dart'; +import 'package:syphon/global/print.dart'; import 'package:syphon/global/values.dart'; abstract class Users { /// Fetch Account Data - /// + /// /// https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-user-userid-account-data-type - /// + /// /// Set some account_data for the client. This config is only visible - /// to the user that set the account_data. The config will be synced + /// to the user that set the account_data. The config will be synced /// to clients in the top-level account_data. static Future fetchAccountData({ String? protocol = 'https://', @@ -42,11 +40,11 @@ abstract class Users { } /// Save Account Data - /// + /// /// https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-user-userid-account-data-type - /// + /// /// Set some account_data for the client. This config is only visible - /// to the user that set the account_data. The config will be synced + /// to the user that set the account_data. The config will be synced /// to clients in the top-level account_data. static Future saveAccountData({ String? protocol = 'https://', @@ -75,11 +73,11 @@ abstract class Users { } /// Ignore User (a.k.a. Block User) - /// + /// /// https://matrix.org/docs/spec/client_server/latest#m-ignored-user-list - /// + /// /// Set some account_data for the client. This config is only visible - /// to the user that set the account_data. The config will be synced + /// to the user that set the account_data. The config will be synced /// to clients in the top-level account_data. static Future updateBlockedUsers({ String? protocol = 'https://', @@ -111,11 +109,11 @@ abstract class Users { } /// Ignore User (a.k.a. Block User) - /// + /// /// https://matrix.org/docs/spec/client_server/latest#m-ignored-user-list - /// + /// /// Set some account_data for the client. This config is only visible - /// to the user that set the account_data. The config will be synced + /// to the user that set the account_data. The config will be synced /// to clients in the top-level account_data. static Future inviteUser({ String? protocol = 'https://', @@ -124,7 +122,8 @@ abstract class Users { String? roomId, String? userId, }) async { - final String url = '$protocol$homeserver/_matrix/client/r0/rooms/$roomId/invite'; + final String url = + '$protocol$homeserver/_matrix/client/r0/rooms/$roomId/invite'; final Map headers = { 'Authorization': 'Bearer $accessToken', @@ -145,11 +144,11 @@ abstract class Users { } /// Update Display Name - /// + /// /// https://matrix.org/docs/spec/client_server/latest#id260 - /// + /// /// This API sets the given user's display name. - /// You must have permission to set this user's display name, + /// You must have permission to set this user's display name, /// e.g. you need to have their access_token. static Future fetchUserProfile({ String? protocol = 'https://', @@ -174,11 +173,11 @@ abstract class Users { } /// Update Display Name - /// + /// /// https://matrix.org/docs/spec/client_server/latest#id260 - /// + /// /// This API sets the given user's display name. - /// You must have permission to set this user's display name, + /// You must have permission to set this user's display name, /// e.g. you need to have their access_token. static Future updateDisplayName({ String? protocol = 'https://', @@ -211,11 +210,11 @@ abstract class Users { } /// Update Avatar Uri - /// + /// /// https://matrix.org/docs/spec/client_server/latest#id303 - /// - /// This API sets the given user's avatar URL. - /// You must have permission to set this user's avatar URL, e.g. + /// + /// This API sets the given user's avatar URL. + /// You must have permission to set this user's avatar URL, e.g. /// you need to have their access_token. static Future updateAvatarUri({ String? protocol = 'https://', @@ -245,12 +244,59 @@ abstract class Users { saveResponse.body, ); } + + /// Update Avatar Uri + /// + /// https://matrix.org/docs/spec/client_server/latest#id303 + /// + /// This API sets the given user's avatar URL. + /// You must have permission to set this user's avatar URL, e.g. + /// you need to have their access_token. + static Future deactivateUser({ + String? protocol = 'https://', + String? homeserver = 'matrix.org', + String? accessToken, + String? userId, + String? identityServer, + String? session, + String? authType, + String? authValue, + }) async { + final String url = + '$protocol$homeserver/_matrix/client/r0/account/deactivate'; + + final Map headers = { + 'Authorization': 'Bearer $accessToken', + ...Values.defaultHeaders, + }; + + final Map body = {'id_server': identityServer}; + + if (session != null) { + body['auth'] = { + 'session': session, + 'type': authType, + 'user': userId, + 'password': authValue, // WARNING: this may not always be password? + }; + } + + final saveResponse = await http.post( + Uri.parse(url), + headers: headers, + body: json.encode(body), + ); + + return await json.decode( + saveResponse.body, + ); + } } /// https://matrix.org/docs/spec/client_server/latest#id259 -/// -/// A list of members of the room. -/// If you are joined to the room then this will be the current members of the room. +/// +/// A list of members of the room. +/// If you are joined to the room then this will be the current members of the room. /// If you have left the room then this will be the members of the room when you left. dynamic buildRoomMembersRequest({ String protocol = 'https://', @@ -258,7 +304,8 @@ dynamic buildRoomMembersRequest({ String? accessToken, String? roomId, }) { - final String url = '$protocol$homeserver/_matrix/client/r0/rooms/$roomId/members'; + final String url = + '$protocol$homeserver/_matrix/client/r0/rooms/$roomId/members'; final Map headers = {'Authorization': 'Bearer $accessToken'}; diff --git a/lib/global/libs/matrix/utils.dart b/lib/global/libs/matrix/utils.dart index 9a881912a..d0dc541a1 100644 --- a/lib/global/libs/matrix/utils.dart +++ b/lib/global/libs/matrix/utils.dart @@ -1,6 +1,5 @@ -// Dart imports: -import 'dart:math'; import 'dart:convert'; +import 'dart:math'; String generateClientSecret({required int length}) { final random = Random.secure(); diff --git a/lib/global/notifications.dart b/lib/global/notifications.dart index aa5f67271..7c1123a6c 100644 --- a/lib/global/notifications.dart +++ b/lib/global/notifications.dart @@ -1,16 +1,12 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; -// Flutter imports: import 'package:flutter/material.dart'; -// Package imports: import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -// Project imports: import 'package:syphon/global/strings.dart'; import 'package:syphon/global/values.dart'; import 'package:syphon/store/settings/notification-settings/model.dart'; diff --git a/lib/global/strings.dart b/lib/global/strings.dart index 2a0f9267a..a10f6e22e 100644 --- a/lib/global/strings.dart +++ b/lib/global/strings.dart @@ -1,6 +1,12 @@ -// Project imports: import 'package:syphon/global/values.dart'; +// Use this to reference JSON defined +// string IDs for i18n library reference +class StringIds { + static const titleConfirmPassword = 'title-confirm-password'; + static const promptConfirmDeactivation = 'prompt-confirm-deactivate'; +} + /// Will be converted to /// i18n json soon, but a "String" /// class below is just a stub for now @@ -57,8 +63,10 @@ class Strings { static const buttonLetsChat = 'let\'s chat'; static const buttonCreate = 'create'; static const buttonCancel = 'cancel'; + static const buttonDeactivate = 'deactivate'; static const buttonQuit = 'quit'; static const buttonConfirm = 'got it'; + static const buttonConfirmOfficial = 'confirm'; static const buttonConfirmAlt = 'ok'; static const buttonBlocKUser = 'block user'; static const buttonDeleteKeys = 'delete keys'; @@ -73,9 +81,13 @@ class Strings { static const buttonLoginCreateAction = 'Create One'; static const buttonTextLogin = 'Login'; + // Prompts + static const passwordRecommendationDefault = + 'Try thinking up 3 or more random\nwords you\'ll easily remember'; + // Errors static const alertInviteUnknownUser = - 'This user doens\'t appear to exist within matrix, but you can attempt to invite them anyway.\n\nMake sure you have the correct name before trying.'; + 'This user doesn\'t appear to exist within matrix, but you can attempt to invite them anyway.\n\nMake sure you have the correct name before trying.'; static const errorMessageSendingFailed = 'Message Failed To Send'; static const errorCheckHomeserver = 'This server failed the \'well-known\' check, make sure the server is configured correctly'; @@ -105,6 +117,13 @@ class Strings { static const contentDeleteDeviceKeyWarning = "Are you sure you want to export this devices encryption key? It may make it available to others if you're not careful!"; + + static const contentDeactivateAccount = + 'THIS WILL PERMANENTLY DELETE YOUR ACCOUNT\n\nYou will be unable to recover any data or access for this account after deactivation.\nPlease take careful consideration before doing this!'; + + static const contentDeactivateAccountFinal = + 'There is no way to recover this account after it\'s deleted. You will immediately be logged out of your account and it will become unavailable.\n\nThis is your final warning regarding deactivation. If you are sure you\'re sure, press deactivate below.'; + static const contentEncryptedMessage = 'Encrypted Message'; static const contentDeletedMessage = 'This message was deleted'; @@ -115,7 +134,10 @@ class Strings { 'A verification email will be sent to your inbox before resetting your password. After verification, you\'ll be able to set and confirm a new password.'; static const contentConfirmPasswordReset = - 'Click on the link sent to your email. After clicking the link, press the continue button below to change your passwoord.'; + 'Click on the link sent to your email. After clicking the link, press the continue button below to change your password.'; + + static const contentPasswordRequirements = + 'Each homeserver may have different requirements for passwords.\n\nIf you\'re having trouble, try creating a password including a lower-case letter, an upper-case letter, a number and a symbol and with at least 8 characters'; static const contentEmailVerifiedRequirement = 'This homeserver requires a verified email to complete registration, you\'ll need to click the link in the email address to continue. Make sure you trust this homeserver before clicking the verification link.'; diff --git a/lib/global/themes.dart b/lib/global/themes.dart index 9cd29f16b..5e3c0c691 100644 --- a/lib/global/themes.dart +++ b/lib/global/themes.dart @@ -1,4 +1,3 @@ -// Flutter imports: import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -51,11 +50,11 @@ class Themes { static Color? backgroundBrightness(ThemeType type) { switch (type) { case ThemeType.LIGHT: - return Colors.grey[200]; + return Color(Colours.greyLightest); case ThemeType.NIGHT: - return Colors.grey[500]; + return Color(Colours.greyDefault); default: - return Colors.grey[700]; + return Color(Colours.greyDark); } } @@ -81,7 +80,7 @@ class Themes { var modalColor; var appBarElevation; var brightness = Brightness.light; - var iconColor = Colors.grey[500]; + var iconColor = Color(Colours.greyDefault); switch (themeType) { case ThemeType.DARK: @@ -154,8 +153,7 @@ class Themes { break; } - final invertedPrimaryColor = - brightness == Brightness.light ? primaryColor : accentColor; + final invertedPrimaryColor = brightness == Brightness.light ? primaryColor : accentColor; return ThemeData( // Main Colors @@ -163,7 +161,13 @@ class Themes { primaryColorDark: Color(primaryColor), primaryColorLight: Color(primaryColor), accentColor: Color(accentColor), + accentIconTheme: IconThemeData(color: Color(accentColor)), brightness: brightness, + colorScheme: ThemeData().colorScheme.copyWith( + primary: Color(primaryColor), + secondary: Color(accentColor), + brightness: brightness, + ), // Core UI\ dialogBackgroundColor: modalColor, @@ -174,9 +178,7 @@ class Themes { selectionHandleColor: Color(primaryColor), ), iconTheme: IconThemeData(color: iconColor), - scaffoldBackgroundColor: scaffoldBackgroundColor != null - ? Color(scaffoldBackgroundColor) - : null, + scaffoldBackgroundColor: scaffoldBackgroundColor != null ? Color(scaffoldBackgroundColor) : null, appBarTheme: AppBarTheme( elevation: appBarElevation, brightness: Brightness.dark, @@ -186,6 +188,7 @@ class Themes { helperStyle: TextStyle( color: Color(invertedPrimaryColor), ), + focusColor: Color(invertedPrimaryColor), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(28.0), borderSide: BorderSide( diff --git a/lib/global/values.dart b/lib/global/values.dart index a3f455e2f..b934bba6a 100644 --- a/lib/global/values.dart +++ b/lib/global/values.dart @@ -60,7 +60,7 @@ class Values { static const animationDurationDefaultFast = 275; static const serviceNotificationTimeoutDuration = 75000; // millis - static const defaultHeaders = {'Content-type': 'application/json'}; + static const defaultHeaders = {'Content-Type': 'application/json'}; static const fontFamilies = [ 'Rubik', 'Roboto', diff --git a/lib/main.dart b/lib/main.dart index 16cc1d682..b90835c4b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,10 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:easy_localization/easy_localization.dart' as localization; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -// Package imports: import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; import 'package:sembast/sembast.dart'; @@ -17,7 +14,6 @@ import 'package:syphon/global/notifications.dart'; import 'package:syphon/global/platform.dart'; import 'package:syphon/storage/index.dart'; -// Project imports: import 'package:syphon/global/themes.dart'; import 'package:syphon/store/alerts/actions.dart'; import 'package:syphon/store/auth/actions.dart'; @@ -153,9 +149,7 @@ class SyphonState extends State with WidgetsBindingObserver { if (user == null && defaultHome.runtimeType == HomeScreen) { defaultHome = IntroScreen(); NavigationService.clearTo('/intro', context); - } else if (user != null && - user.accessToken != null && - defaultHome.runtimeType == IntroScreen) { + } else if (user != null && user.accessToken != null && defaultHome.runtimeType == IntroScreen) { // Default Authenticated App Home defaultHome = HomeScreen(); NavigationService.clearTo('/home', context); @@ -181,17 +175,13 @@ class SyphonState extends State with WidgetsBindingObserver { color = Colors.grey; } - final alertMessage = - alert.message ?? alert.error ?? 'Unknown Error Occured'; + final alertMessage = alert.message ?? alert.error ?? 'Unknown Error Occured'; globalScaffold.currentState?.showSnackBar(SnackBar( backgroundColor: color, content: Text( alertMessage, - style: Theme.of(context) - .textTheme - .subtitle1 - ?.copyWith(color: Colors.white), + style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.white), ), duration: alert.duration, action: SnackBarAction( @@ -207,18 +197,13 @@ class SyphonState extends State with WidgetsBindingObserver { @override void dispose() { - alertsListener?.cancel(); + store.dispatch(stopAuthObserver()); + store.dispatch(stopAlertsObserver()); store.dispatch(disposeDeepLinks()); - super.dispose(); - } - - @override - void deactivate() { closeCache(cache); WidgetsBinding.instance?.removeObserver(this); - store.dispatch(stopAuthObserver()); - store.dispatch(stopAlertsObserver()); - super.deactivate(); + alertsListener?.cancel(); + super.dispose(); } // Store should not need to be passed to a widget to affect @@ -229,8 +214,7 @@ class SyphonState extends State with WidgetsBindingObserver { child: localization.EasyLocalization( path: 'assets/translations', useOnlyLangCode: true, - startLocale: - Locale(formatLanguageCode(store.state.settingsStore.language)), + startLocale: Locale(formatLanguageCode(store.state.settingsStore.language)), fallbackLocale: Locale('en'), supportedLocales: const [Locale('en'), Locale('ru')], child: StoreConnector( diff --git a/lib/storage/middleware.dart b/lib/storage/middleware.dart index bea64ba0a..1fb486a1c 100644 --- a/lib/storage/middleware.dart +++ b/lib/storage/middleware.dart @@ -44,15 +44,19 @@ dynamic storageMiddleware( case UpdateRoom: // TODO: create a mutation like SetSyncing to distinguish small but important room mutations if (action.syncing == null) { - // printInfo( - // '[storageMiddleware] saving room ${action.runtimeType.toString()}', - // ); final room = store.state.roomStore.rooms[action.id]; if (room != null) { saveRoom(room, storage: Storage.main); } } break; + case RemoveRoom: + final _action = action as RemoveRoom; + final room = store.state.roomStore.rooms[_action.roomId]; + if (room != null) { + deleteRooms({room.id: room}, storage: Storage.main); + } + break; case SetTheme: case SetPrimaryColor: case SetAvatarShape: diff --git a/lib/store/alerts/actions.dart b/lib/store/alerts/actions.dart index 0c255ff0b..155dd5a67 100644 --- a/lib/store/alerts/actions.dart +++ b/lib/store/alerts/actions.dart @@ -1,15 +1,11 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/store/index.dart'; import './model.dart'; diff --git a/lib/store/alerts/model.dart b/lib/store/alerts/model.dart index 01191763c..2660f4740 100644 --- a/lib/store/alerts/model.dart +++ b/lib/store/alerts/model.dart @@ -1,7 +1,5 @@ -// Dart imports: import 'dart:async'; -// Package imports: import 'package:equatable/equatable.dart'; class Alert { diff --git a/lib/store/alerts/reducer.dart b/lib/store/alerts/reducer.dart index 3efc41331..c646d2f29 100644 --- a/lib/store/alerts/reducer.dart +++ b/lib/store/alerts/reducer.dart @@ -1,4 +1,3 @@ -// Project imports: import './actions.dart'; import './model.dart'; diff --git a/lib/store/alerts/selectors.dart b/lib/store/alerts/selectors.dart index 3fcd25621..6f6b7a6de 100644 --- a/lib/store/alerts/selectors.dart +++ b/lib/store/alerts/selectors.dart @@ -1,4 +1,3 @@ -// Project imports: import 'package:syphon/store/index.dart'; import './model.dart'; diff --git a/lib/store/auth/actions.dart b/lib/store/auth/actions.dart index 8b7a2dc03..e201a71ff 100644 --- a/lib/store/auth/actions.dart +++ b/lib/store/auth/actions.dart @@ -1,16 +1,13 @@ -// Dart imports: import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; -// Flutter imports: import 'package:flutter/material.dart'; -// Package imports: -import 'package:crypt/crypt.dart'; import 'package:device_info/device_info.dart'; import 'package:redux/redux.dart'; @@ -18,7 +15,6 @@ import 'package:redux_thunk/redux_thunk.dart'; import 'package:syphon/cache/index.dart'; import 'package:syphon/global/libs/jack/index.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/auth.dart'; import 'package:syphon/global/libs/matrix/errors.dart'; import 'package:syphon/global/libs/matrix/index.dart'; @@ -40,11 +36,8 @@ import 'package:syphon/store/rooms/actions.dart'; import 'package:syphon/store/search/actions.dart'; import 'package:syphon/store/settings/actions.dart'; import 'package:syphon/store/settings/devices-settings/model.dart'; -import 'package:syphon/store/settings/notification-settings/actions.dart'; -import 'package:syphon/store/settings/notification-settings/model.dart'; import 'package:syphon/store/settings/notification-settings/remote/actions.dart'; import 'package:syphon/store/sync/actions.dart'; -import 'package:syphon/store/sync/background/service.dart'; import 'package:syphon/store/sync/background/storage.dart'; import 'package:syphon/store/user/actions.dart'; import 'package:uni_links/uni_links.dart'; @@ -67,8 +60,8 @@ class SetUser { } class SetClientSecret { - final String? clientSecret; - SetClientSecret({this.clientSecret}); + final String clientSecret; + SetClientSecret({required this.clientSecret}); } class SetHostname { @@ -182,6 +175,8 @@ class ResetOnboarding {} class ResetAuthStore {} +class ResetSession {} + late StreamSubscription _sub; ThunkAction initDeepLinks() => (Store store) async { @@ -201,8 +196,7 @@ ThunkAction initDeepLinks() => (Store store) async { } on PlatformException { addAlert( origin: 'initDeepLinks', - message: - 'Failed to SSO Login, please try again later or contact support', + message: 'Failed to SSO Login, please try again later or contact support', ); // Handle exception by warning the user their action did not succeed // return? @@ -225,7 +219,7 @@ ThunkAction startAuthObserver() { authObserver: StreamController.broadcast(), )); - final Function onAuthStateChanged = (User? user) async { + final onAuthStateChanged = (User? user) async { if (user != null && user.accessToken != null) { await store.dispatch(fetchAuthUserProfile()); @@ -274,7 +268,7 @@ ThunkAction startAuthObserver() { // set auth state listener store.state.authStore.onAuthStateChanged!.listen( - onAuthStateChanged as Function(User?), + onAuthStateChanged, ); }; } @@ -294,26 +288,13 @@ ThunkAction stopAuthObserver() { /// for encryption and verification ThunkAction generateDeviceId({String? salt}) { return (Store store) async { - // Wait at least 2 seconds until you can attempt to login again - // includes processing time by authenticating matrix server - store.dispatch(SetStopgap(stopgap: true)); - - // prevents people spamming the login if it were to fail repeatedly - Timer(Duration(seconds: 2), () { - store.dispatch(SetStopgap(stopgap: false)); - }); - final defaultId = Random.secure().nextInt(1 << 31).toString(); - var device = Device( - deviceId: defaultId, - displayName: Values.appDisplayName, - ); - - var deviceId; try { final deviceInfoPlugin = DeviceInfoPlugin(); + var deviceId; + // Find a unique value for the type of device if (Platform.isAndroid) { final info = await deviceInfoPlugin.androidInfo; @@ -326,24 +307,21 @@ ThunkAction generateDeviceId({String? salt}) { } // hash it - final cryptHash = Crypt.sha256(deviceId, rounds: 1000, salt: salt).hash; + final deviceIdDigest = sha256.convert(utf8.encode(deviceId + salt)); - // make it easier to read - final deviceIdHash = cryptHash - .toUpperCase() - .replaceAll(RegExp(r'[^\w]'), '') - .substring(0, 10); + final deviceIdHash = + base64.encode(deviceIdDigest.bytes).toUpperCase().replaceAll(RegExp(r'[^\w]'), '').substring(0, 10); - device = Device( + return Device( deviceId: deviceIdHash, - deviceIdPrivate: deviceId, displayName: Values.appDisplayName, ); - - return device; } catch (error) { debugPrint('[generateDeviceId] $error'); - return device; + return Device( + deviceId: defaultId, + displayName: Values.appDisplayName, + ); } }; } @@ -353,6 +331,15 @@ ThunkAction loginUser() { store.dispatch(SetLoading(loading: true)); try { + // Wait at least 2 seconds until you can attempt to login again + // includes processing time by authenticating matrix server + store.dispatch(SetStopgap(stopgap: true)); + + // prevents people spamming the login if it were to fail repeatedly + Timer(Duration(seconds: 2), () { + store.dispatch(SetStopgap(stopgap: false)); + }); + var homeserver = store.state.authStore.homeserver; final username = store.state.authStore.username.replaceAll('@', ''); final password = store.state.authStore.password; @@ -425,7 +412,15 @@ ThunkAction loginUserSSO({String? token}) { } final username = store.state.authStore.username; - final protocol = store.state.authStore.protocol; + + // Wait at least 2 seconds until you can attempt to login again + // includes processing time by authenticating matrix server + store.dispatch(SetStopgap(stopgap: true)); + + // prevents people spamming the login if it were to fail repeatedly + Timer(Duration(seconds: 2), () { + store.dispatch(SetStopgap(stopgap: false)); + }); final Device device = await store.dispatch( generateDeviceId(salt: username), @@ -481,6 +476,8 @@ ThunkAction logoutUser() { if (store.state.authStore.user.homeserver == null) { throw Exception('Unavailable user data'); } + + // tell authObserver to wipe store user and other data final temp = store.state.authStore.user.accessToken; store.state.authStore.authObserver!.add(null); @@ -497,6 +494,9 @@ ThunkAction logoutUser() { throw Exception(data['error']); } } + + store.state.authStore.authObserver!.add(null); + // wipe cache await deleteCache(); await initCache(); @@ -505,8 +505,8 @@ ThunkAction logoutUser() { await deleteStorage(); await initStorage(); - // // tell authObserver to wipe store user and other data - // store.state.authStore.authObserver!.add(null); + // reset client secret + await store.dispatch(initClientSecret()); } catch (error) { store.dispatch(addAlert( origin: 'logoutUser', @@ -579,8 +579,7 @@ ThunkAction checkUsernameAvailability() { ThunkAction setInteractiveAuths({Map? auths}) { return (Store store) async { try { - final List completed = - List.from(auths!['completed'] ?? []); + final List completed = List.from(auths!['completed'] ?? []); await store.dispatch(SetSession(session: auths['session'])); await store.dispatch(SetCompleted(completed: completed)); @@ -629,7 +628,7 @@ ThunkAction checkPasswordResetVerification({ final protocol = store.state.authStore.protocol; final data = await MatrixApi.resetPassword( - protocol: store.state.authStore.protocol, + protocol: protocol, homeserver: homeserver, clientSecret: clientSecret, sendAttempt: sendAttempt, @@ -637,8 +636,7 @@ ThunkAction checkPasswordResetVerification({ session: session, ); - if (data['errcode'] != null && - data['errcode'] == MatrixErrors.not_authorized) { + if (data['errcode'] != null && data['errcode'] == MatrixErrors.not_authorized) { throw data['error']; } @@ -670,7 +668,7 @@ ThunkAction resetPassword({int sendAttempt = 1, String? password}) { final protocol = store.state.authStore.protocol; final data = await MatrixApi.resetPassword( - protocol: store.state.authStore.protocol, + protocol: protocol, homeserver: homeserver, clientSecret: clientSecret, sendAttempt: sendAttempt, @@ -744,13 +742,12 @@ ThunkAction submitEmail({int? sendAttempt = 1}) { final currentCredential = store.state.authStore.credential!; final protocol = store.state.authStore.protocol; - if (currentCredential.params!.containsValue(emailSubmitted) && - sendAttempt! < 2) { + if (currentCredential.params!.containsValue(emailSubmitted) && sendAttempt! < 2) { return true; } final data = await MatrixApi.registerEmail( - protocol: store.state.authStore.protocol, + protocol: protocol, homeserver: homeserver, email: store.state.authStore.email, clientSecret: clientSecret, @@ -818,8 +815,7 @@ ThunkAction createUser({enableErrors = false}) { ); if (data['errcode'] != null) { - if (data['errcode'] == MatrixErrors.not_authorized && - credential!.type == MatrixAuthTypes.EMAIL) { + if (data['errcode'] == MatrixErrors.not_authorized && credential!.type == MatrixAuthTypes.EMAIL) { store.dispatch(SetVerificationNeeded(needed: true)); return false; } @@ -829,8 +825,7 @@ ThunkAction createUser({enableErrors = false}) { if (data['flows'] != null) { await store.dispatch(setInteractiveAuths(auths: data)); - final List stages = - store.state.authStore.interactiveAuths['flows'][0]['stages']; + final List stages = store.state.authStore.interactiveAuths['flows'][0]['stages']; final completed = store.state.authStore.completed; // Compare the completed stages to the flow stages provided @@ -851,10 +846,11 @@ ThunkAction createUser({enableErrors = false}) { return true; } catch (error) { printError('[createUser] error $error'); + if (enableErrors) { store.dispatch(addAlert( origin: 'createUser', - message: 'Failed to signup', + message: error.toString(), error: error, )); } @@ -873,8 +869,6 @@ ThunkAction updatePassword(String password) { var data; - final protocol = store.state.authStore.protocol; - // Call just to get interactive auths data = await MatrixApi.updatePassword( protocol: store.state.authStore.protocol, @@ -1027,16 +1021,9 @@ ThunkAction updateCredential({ }; } -ThunkAction resetCredentials() { +ThunkAction resetInteractiveAuth() { return (Store store) async { - store.dispatch(SetSession(session: null)); - store.dispatch(SetCredential(credential: null)); - }; -} - -ThunkAction resetSession() { - return (Store store) async { - store.dispatch(SetSession(session: '')); + store.dispatch(ResetSession()); }; } @@ -1053,35 +1040,78 @@ ThunkAction selectHomeserver({String? hostname}) { }; } +ThunkAction deactivateAccount() => (Store store) async { + try { + store.dispatch(SetLoading(loading: true)); + + final currentCredential = store.state.authStore.credential ?? Credential(); + + final idServer = store.state.authStore.user.idserver; + final homeserver = store.state.authStore.user.homeserver; + + final data = await MatrixApi.deactivateUser( + protocol: store.state.authStore.protocol, + homeserver: homeserver, + accessToken: store.state.authStore.user.accessToken, + identityServer: idServer ?? homeserver, + session: store.state.authStore.session, + userId: store.state.authStore.user.userId, + authType: MatrixAuthTypes.PASSWORD, + authValue: currentCredential.value, + ); + + if (data['errcode'] != null) { + throw data['error']; + } + + if (data['flows'] != null) { + return store.dispatch(setInteractiveAuths(auths: data)); + } + + store.state.authStore.authObserver!.add(null); + + // wipe cache + await deleteCache(); + await initCache(); + + // wipe cold storage + await deleteStorage(); + await initStorage(); + + // reset client secret + await store.dispatch(initClientSecret()); + } catch (error) { + store.dispatch(addAlert( + error: error, + message: error.toString(), + origin: 'deactivateAccount', + )); + } finally { + store.dispatch(SetLoading(loading: false)); + } + }; + ThunkAction fetchHomeservers() { return (Store store) async { store.dispatch(SetLoading(loading: true)); - final List homeserversJson = - await (JackApi.fetchPublicServers() as Future>); + final homeserversJson = await JackApi.fetchPublicServers(); // parse homeserver data final List homserverData = homeserversJson.map((data) { final hostname = data['hostname'].toString().split('.'); final hostnameBase = hostname.length > 1 - ? hostname[hostname.length - 2] + '.' + hostname[hostname.length - 1] + ? '${hostname[hostname.length - 2]}.${hostname[hostname.length - 1]}' : hostname[0]; return Homeserver( hostname: hostnameBase, location: data['location'] ?? '', description: data['description'] ?? '', - usersActive: data['users_active'] != null - ? data['users_active'].toString() - : null, - roomsTotal: data['public_room_count'] != null - ? data['public_room_count'].toString() - : null, - founded: - data['online_since'] != null ? data['online_since'].toString() : '', - responseTime: data['last_response_time'] != null - ? data['last_response_time'].toString() - : '', + usersActive: data['users_active'] != null ? data['users_active'].toString() : null, + roomsTotal: data['public_room_count'] != null ? data['public_room_count'].toString() : null, + founded: data['online_since'] != null ? data['online_since'].toString() : '', + responseTime: data['last_response_time'] != null ? data['last_response_time'].toString() : '', ); }).toList(); @@ -1092,8 +1122,8 @@ ThunkAction fetchHomeservers() { final homeservers = await Future.wait( homserverData.map((homeserver) async { final url = await fetchFavicon(url: homeserver.hostname); - final uri = Uri.parse(url!); try { + final uri = Uri.parse(url!); final response = await http.get(uri); if (response.statusCode == 200) { @@ -1164,20 +1194,17 @@ ThunkAction fetchHomeserver({String? hostname}) { }; } -ThunkAction initClientSecret({String? hostname}) => - (Store store) { +ThunkAction initClientSecret({String? hostname}) => (Store store) { store.dispatch(SetClientSecret( clientSecret: generateClientSecret(length: 24), )); }; -ThunkAction setHostname({String? hostname}) => - (Store store) { +ThunkAction setHostname({String? hostname}) => (Store store) { store.dispatch(SetHostname(hostname: hostname!.trim())); }; -ThunkAction setHomeserver({Homeserver? homeserver}) => - (Store store) { +ThunkAction setHomeserver({Homeserver? homeserver}) => (Store store) { store.dispatch(SetHomeserver(homeserver: homeserver)); }; @@ -1195,8 +1222,7 @@ ThunkAction setEmail({String? email}) { ThunkAction setUsername({String? username}) { return (Store store) { - store.dispatch( - SetUsernameValid(valid: username != null && username.isNotEmpty)); + store.dispatch(SetUsernameValid(valid: username != null && username.isNotEmpty)); store.dispatch(SetUsername(username: username!.trim())); }; } @@ -1227,17 +1253,15 @@ ThunkAction resolveUsername({String? username}) { }; } -ThunkAction setLoginPassword({String? password}) => - (Store store) { +ThunkAction setLoginPassword({String? password}) => (Store store) { store.dispatch(SetPassword(password: password)); - store.dispatch(SetPasswordValid( valid: password != null && password.isNotEmpty, )); }; ThunkAction setPassword({ - String? password, + required String password, bool ignoreConfirm = false, }) { return (Store store) { @@ -1247,10 +1271,7 @@ ThunkAction setPassword({ final currentConfirm = store.state.authStore.passwordConfirm; store.dispatch(SetPasswordValid( - valid: password != null && - currentConfirm != null && - password.length > 6 && - (currentPassword == currentConfirm || ignoreConfirm), + valid: (currentPassword == currentConfirm || ignoreConfirm) && password.length > 8, )); }; } @@ -1269,9 +1290,7 @@ ThunkAction setPasswordConfirm({String? password}) { final currentConfirm = store.state.authStore.passwordConfirm; store.dispatch(SetPasswordValid( - valid: password != null && - password.length > 6 && - currentPassword == currentConfirm, + valid: password != null && password.length > 6 && currentPassword == currentConfirm, )); }; } diff --git a/lib/store/auth/credential/model.dart b/lib/store/auth/credential/model.dart index 41240aab0..22d5a76b4 100644 --- a/lib/store/auth/credential/model.dart +++ b/lib/store/auth/credential/model.dart @@ -1,8 +1,6 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/auth.dart'; part 'model.g.dart'; diff --git a/lib/store/auth/homeserver/actions.dart b/lib/store/auth/homeserver/actions.dart index f76a0f068..e52299ed9 100644 --- a/lib/store/auth/homeserver/actions.dart +++ b/lib/store/auth/homeserver/actions.dart @@ -1,10 +1,6 @@ -// Dart imports: - -// Package imports: import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/store/auth/homeserver/model.dart'; import 'package:syphon/store/index.dart'; diff --git a/lib/store/auth/homeserver/model.dart b/lib/store/auth/homeserver/model.dart index dbe331b48..8bf348044 100644 --- a/lib/store/auth/homeserver/model.dart +++ b/lib/store/auth/homeserver/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/store/auth/reducer.dart b/lib/store/auth/reducer.dart index 0724376b5..5d7ef049d 100644 --- a/lib/store/auth/reducer.dart +++ b/lib/store/auth/reducer.dart @@ -1,4 +1,3 @@ -// Project imports: import '../user/model.dart'; import './actions.dart'; import './state.dart'; @@ -67,9 +66,18 @@ AuthStore authReducer([AuthStore state = const AuthStore(), dynamic action]) { captcha: false, interactiveAuths: null, ); + case ResetSession: + return AuthStore( + loading: false, + user: state.user, + clientSecret: state.clientSecret, + ); case ResetAuthStore: // retain the app sessions auth observer only - return AuthStore(authObserver: state.authObserver); + return AuthStore( + authObserver: state.authObserver, + clientSecret: state.clientSecret, + ); default: return state; } diff --git a/lib/store/auth/selectors.dart b/lib/store/auth/selectors.dart index 21307a412..b5a4cb4e9 100644 --- a/lib/store/auth/selectors.dart +++ b/lib/store/auth/selectors.dart @@ -1,4 +1,3 @@ -// Project imports: import 'package:syphon/global/libs/matrix/auth.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/user/model.dart'; diff --git a/lib/store/auth/state.dart b/lib/store/auth/state.dart index 564036baf..2f277197e 100644 --- a/lib/store/auth/state.dart +++ b/lib/store/auth/state.dart @@ -1,12 +1,9 @@ -// Dart imports: import 'dart:async'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:syphon/global/libs/matrix/auth.dart'; -// Project imports: import 'package:syphon/global/values.dart'; import 'package:syphon/store/auth/credential/model.dart'; import 'package:syphon/store/auth/homeserver/model.dart'; @@ -40,6 +37,7 @@ class AuthStore extends Equatable { final List completed; final Map interactiveAuths; + // TODO: extract / cache in case user force closes app during signup // temp state values for signup final String email; final String username; @@ -128,8 +126,8 @@ class AuthStore extends Equatable { AuthStore copyWith({ user, - session, - clientSecret, + String? session, + String? clientSecret, protocol, email, loading, diff --git a/lib/store/crypto/actions.dart b/lib/store/crypto/actions.dart index 349c14ff8..08169a05d 100644 --- a/lib/store/crypto/actions.dart +++ b/lib/store/crypto/actions.dart @@ -1,14 +1,11 @@ -// Dart imports: import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; -// Flutter imports: import 'package:canonical_json/canonical_json.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:file_picker/file_picker.dart'; import 'package:intl/intl.dart'; @@ -16,10 +13,8 @@ import 'package:olm/olm.dart' as olm; import 'package:path_provider/path_provider.dart'; import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -import 'package:syphon/global/algos.dart'; import 'package:syphon/global/libs/matrix/constants.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/encryption.dart'; import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/global/print.dart'; @@ -291,8 +286,6 @@ ThunkAction generateIdentityKeys() { }; // fingerprint signature key pair generation for upload - // warn: seems to work without canonical_json lib - // utf8.decode(deviceKeysEncoded); final deviceKeysEncoded = canonicalJson.encode(deviceIdentityKeys); final deviceKeysSerialized = utf8.decode(deviceKeysEncoded); final deviceKeysSigned = olmAccount.sign(deviceKeysSerialized); @@ -417,11 +410,15 @@ ThunkAction updateOneTimeKeyCounts( return; } + final deviceKeysOwned = store.state.cryptoStore.deviceKeysOwned.isEmpty; + if (deviceKeysOwned) { + return; + } + // if the key count hasn't changed, don't update it final currentKeyCount = store.state.cryptoStore.oneTimeKeysCounts; - if (currentKeyCount[Algorithms.signedcurve25519] == - oneTimeKeysCounts[Algorithms.signedcurve25519] && + if (currentKeyCount[Algorithms.signedcurve25519] == oneTimeKeysCounts[Algorithms.signedcurve25519] && currentKeyCount.isNotEmpty) { return; } @@ -432,8 +429,7 @@ ThunkAction updateOneTimeKeyCounts( // register new key counts final int maxKeyCount = olmAccount.max_number_of_one_time_keys(); - final int signedCurveCount = - oneTimeKeysCounts[Algorithms.signedcurve25519] ?? 0; + final int signedCurveCount = oneTimeKeysCounts[Algorithms.signedcurve25519] ?? 0; // the last check is because im scared if ((signedCurveCount < maxKeyCount / 3) && signedCurveCount < 100) { @@ -537,8 +533,7 @@ ThunkAction updateKeySessions({ (oneTimeKey) async { try { // find the identityKey for the device - final deviceKey = store.state.cryptoStore - .deviceKeys[oneTimeKey.userId!]![oneTimeKey.deviceId!]!; + final deviceKey = store.state.cryptoStore.deviceKeys[oneTimeKey.userId!]![oneTimeKey.deviceId!]!; // Poorly decided to save key sessions by deviceId at first but then // realised that you may have the same identityKey for diff @@ -587,10 +582,7 @@ ThunkAction updateKeySessions({ await Future.wait(requestsSendToDevicee); await store.dispatch(setOneTimeKeysClaimed({})); } catch (error) { - store.dispatch(addAlert( - origin: 'updateKeySessions', - message: error.toString(), - error: error)); + store.dispatch(addAlert(origin: 'updateKeySessions', message: error.toString(), error: error)); } }; } @@ -609,9 +601,8 @@ ThunkAction claimOneTimeKeys({ final currentUser = store.state.authStore.user; // get deviceKeys for every user present in the chat - final List roomDeviceKeys = List.from(roomUserIds - .map((userId) => (deviceKeys[userId] ?? {}).values) - .expand((x) => x)); + final List roomDeviceKeys = + List.from(roomUserIds.map((userId) => (deviceKeys[userId] ?? {}).values).expand((x) => x)); // Create a map of all the oneTimeKeys to claim final claimKeysPayload = roomDeviceKeys.fold( @@ -631,8 +622,7 @@ ThunkAction claimOneTimeKeys({ claims[deviceKey.userId] = {}; } - claims[deviceKey.userId][deviceKey.deviceId] = - Algorithms.signedcurve25519; + claims[deviceKey.userId][deviceKey.deviceId] = Algorithms.signedcurve25519; return claims; }, @@ -654,8 +644,7 @@ ThunkAction claimOneTimeKeys({ oneTimeKeys: claimKeysPayload, ); - if (claimKeysResponse['errcode'] != null || - claimKeysResponse['failures'].isNotEmpty) { + if (claimKeysResponse['errcode'] != null || claimKeysResponse['failures'].isNotEmpty) { throw claimKeysResponse['error']; } @@ -693,8 +682,7 @@ ThunkAction claimOneTimeKeys({ // create sessions from new one time keys per device id oneTimekeys.forEach((deviceId, oneTimeKey) { final userId = oneTimeKey.userId; - final deviceKey = - store.state.cryptoStore.deviceKeys[userId!]![deviceId]!; + final deviceKey = store.state.cryptoStore.deviceKeys[userId!]![deviceId]!; final keyId = Keys.identity(deviceId: deviceKey.deviceId); final identityKey = deviceKey.keys![keyId]; @@ -778,8 +766,7 @@ ThunkAction loadKeySessionOutbound({ }) { return (Store store) async { try { - final outboundKeySessionSerialized = - store.state.cryptoStore.outboundKeySessions[identityKey!]; + final outboundKeySessionSerialized = store.state.cryptoStore.outboundKeySessions[identityKey!]; // Deserialize outbound key session with device identity key if (outboundKeySessionSerialized != null) { @@ -812,16 +799,13 @@ ThunkAction loadKeySessionInbound({ return (Store store) async { try { // type 1 - attempt to decrypt with an existing session - final inboundKeySessionSerialized = - store.state.cryptoStore.inboundKeySessions[identityKey!]; + final inboundKeySessionSerialized = store.state.cryptoStore.inboundKeySessions[identityKey!]; if (inboundKeySessionSerialized != null) { - final inboundKeySession = olm.Session() - ..unpickle(identityKey, inboundKeySessionSerialized); + final inboundKeySession = olm.Session()..unpickle(identityKey, inboundKeySessionSerialized); // This returns a flag indicating whether the message was encrypted using that session. - final inboundkeySessionMatch = - inboundKeySession.matches_inbound_from(identityKey, body!); + final inboundkeySessionMatch = inboundKeySession.matches_inbound_from(identityKey, body!); if (inboundkeySessionMatch) { return inboundKeySession; @@ -888,8 +872,7 @@ ThunkAction loadMessageSessionInbound({ String? identityKey, }) { return (Store store) async { - final messageSessions = - store.state.cryptoStore.inboundMessageSessions[roomId!]; + final messageSessions = store.state.cryptoStore.inboundMessageSessions[roomId!]; if (messageSessions == null || !messageSessions.containsKey(identityKey)) { throw 'Unable to find inbound message session for decryption'; @@ -970,8 +953,7 @@ ThunkAction createMessageSessionOutbound({String? roomId}) { ThunkAction loadMessageSessionOutbound({String? roomId}) { return (Store store) async { // Load session for identity - var outboundMessageSessionSerialized = - store.state.cryptoStore.outboundMessageSessions[roomId!]; + var outboundMessageSessionSerialized = store.state.cryptoStore.outboundMessageSessions[roomId!]; if (outboundMessageSessionSerialized == null) { outboundMessageSessionSerialized = await store.dispatch( @@ -999,8 +981,7 @@ ThunkAction saveMessageSessionOutbound({ ThunkAction exportMessageSession({String? roomId}) { return (Store store) async { - final olm.OutboundGroupSession outboundMessageSession = - await store.dispatch( + final olm.OutboundGroupSession outboundMessageSession = await store.dispatch( loadMessageSessionOutbound(roomId: roomId), ); @@ -1077,16 +1058,14 @@ ThunkAction exportDeviceKeysOwned() { final currentTime = DateTime.now(); - final formattedTime = - DateFormat('MMM_dd_yyyy_hh_mm_aa').format(currentTime).toLowerCase(); + final formattedTime = DateFormat('MMM_dd_yyyy_hh_mm_aa').format(currentTime).toLowerCase(); final fileName = '${directory.path}/app_key_export_$formattedTime.json'; var file = File(fileName); final user = store.state.authStore.user; - final deviceKey = - store.state.cryptoStore.deviceKeysOwned[user.deviceId!]!; + final deviceKey = store.state.cryptoStore.deviceKeysOwned[user.deviceId!]!; final exportData = { 'account_key': store.state.cryptoStore.olmAccountKey, diff --git a/lib/store/crypto/events/actions.dart b/lib/store/crypto/events/actions.dart index 4d41d63c6..76cb99498 100644 --- a/lib/store/crypto/events/actions.dart +++ b/lib/store/crypto/events/actions.dart @@ -1,18 +1,14 @@ -// Dart imports: import 'dart:convert'; -// Flutter imports: import 'package:canonical_json/canonical_json.dart'; import 'package:flutter/material.dart'; -// Package imports: // import 'package:canonical_json/canonical_json.dart'; import 'package:olm/olm.dart' as olm; import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; import 'package:syphon/global/algos.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/encryption.dart'; import 'package:syphon/global/print.dart'; import 'package:syphon/store/alerts/actions.dart'; @@ -32,8 +28,7 @@ ThunkAction encryptMessageContent({ }) { return (Store store) async { // Load and deserialize session - final olm.OutboundGroupSession outboundMessageSession = - await store.dispatch( + final olm.OutboundGroupSession outboundMessageSession = await store.dispatch( loadMessageSessionOutbound(roomId: roomId), ); @@ -135,8 +130,7 @@ ThunkAction encryptKeyContent({ final userFingerprintKey = userIdentityKeys[Algorithms.ed25519]; // pull recipient key data and id - final fingerprintKeyId = - Keys.fingerprint(deviceId: recipientKeys!.deviceId); + final fingerprintKeyId = Keys.fingerprint(deviceId: recipientKeys!.deviceId); final identityKeyId = Keys.identity(deviceId: recipientKeys.deviceId); final fingerprintKey = recipientKeys.keys![fingerprintKeyId]; // recipient final identityKey = recipientKeys.keys![identityKeyId]!; // recipient @@ -292,18 +286,18 @@ ThunkAction syncDevice(Map toDeviceRaw) { switch (eventType) { case EventTypes.encrypted: try { + // printJson(toDeviceRaw); // TODO: test olm recovery + final eventDecrypted = await store.dispatch( decryptKeyEvent(event: event), ); if (EventTypes.roomKey == eventDecrypted['type']) { // save decrepted user session key under roomId - await store.dispatch( - saveSessionKey( - event: eventDecrypted, - identityKey: identityKeySender, - ), - ); + await store.dispatch(saveSessionKey( + event: eventDecrypted, + identityKey: identityKeySender, + )); try { // redecrypt events in the room with new key diff --git a/lib/store/crypto/keys/actions.dart b/lib/store/crypto/keys/actions.dart index 1ed76da59..7c3bc0abd 100644 --- a/lib/store/crypto/keys/actions.dart +++ b/lib/store/crypto/keys/actions.dart @@ -1,7 +1,8 @@ -import 'package:crypt/crypt.dart'; +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -import 'package:syphon/global/algos.dart'; import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/global/print.dart'; import 'package:syphon/store/alerts/actions.dart'; @@ -26,7 +27,7 @@ ThunkAction sendKeyRequest({ final String sessionId = event.content['session_id']; // Just needs to be unique, but different - final requestId = Crypt.sha256(sessionId, rounds: 1000, salt: '').hash; + final requestId = sha1.convert(utf8.encode(sessionId)).toString(); final currentUser = store.state.authStore.user; @@ -47,8 +48,6 @@ ThunkAction sendKeyRequest({ if (data['errcode'] != null) { throw data['error']; } - - printDebug('[sendKeyRequest] COMPLETED'); } catch (error) { store.dispatch(addAlert( error: error, diff --git a/lib/store/crypto/keys/model.dart b/lib/store/crypto/keys/model.dart index 691677970..0ea698f4e 100644 --- a/lib/store/crypto/keys/model.dart +++ b/lib/store/crypto/keys/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/store/crypto/model.dart b/lib/store/crypto/model.dart index d90b6d9fa..58b97db3e 100644 --- a/lib/store/crypto/model.dart +++ b/lib/store/crypto/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:syphon/global/libs/matrix/encryption.dart'; diff --git a/lib/store/crypto/reducer.dart b/lib/store/crypto/reducer.dart index 10749f9d6..ce7cd782c 100644 --- a/lib/store/crypto/reducer.dart +++ b/lib/store/crypto/reducer.dart @@ -1,4 +1,3 @@ -// Project imports: import 'package:syphon/global/print.dart'; import './actions.dart'; diff --git a/lib/store/crypto/state.dart b/lib/store/crypto/state.dart index 5ff7d857a..b1e6381c7 100644 --- a/lib/store/crypto/state.dart +++ b/lib/store/crypto/state.dart @@ -1,9 +1,7 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:olm/olm.dart'; -// Project imports: import 'package:syphon/store/crypto/keys/model.dart'; import 'package:syphon/store/crypto/model.dart'; @@ -18,6 +16,10 @@ class CryptoStore extends Equatable { // Serialized olm account final String? olmAccountKey; + final bool? deviceKeysExist; + + final bool deviceKeyVerified; + // Map // megolm - index per chat final Map> messageSessionIndex; @@ -39,8 +41,6 @@ class CryptoStore extends Equatable { // Map deviceKeysOwned final Map deviceKeysOwned; // key is deviceId - final bool? deviceKeysExist; - // Track last known uploaded key amounts final Map oneTimeKeysCounts; @@ -49,6 +49,8 @@ class CryptoStore extends Equatable { const CryptoStore({ this.olmAccount, this.olmAccountKey, + this.deviceKeysExist = false, + this.deviceKeyVerified = false, this.inboundMessageSessions = const {}, // messages this.outboundMessageSessions = const {}, // messages // this.inboundKeySessions = const {}, // one-time device keys @@ -57,7 +59,6 @@ class CryptoStore extends Equatable { this.deviceKeys = const {}, this.deviceKeysOwned = const {}, this.oneTimeKeysClaimed = const {}, - this.deviceKeysExist = false, this.oneTimeKeysCounts = const {}, }); @@ -65,6 +66,8 @@ class CryptoStore extends Equatable { List get props => [ olmAccount, olmAccountKey, + deviceKeysExist, + deviceKeyVerified, messageSessionIndex, inboundMessageSessions, outboundMessageSessions, @@ -72,7 +75,6 @@ class CryptoStore extends Equatable { outboundKeySessions, deviceKeys, deviceKeysOwned, - deviceKeysExist, oneTimeKeysClaimed, oneTimeKeysCounts ]; @@ -80,12 +82,13 @@ class CryptoStore extends Equatable { CryptoStore copyWith({ Account? olmAccount, String? olmAccountKey, + bool? deviceKeysExist, + bool? deviceKeyVerified, Map>? messageSessionIndex, Map>? inboundMessageSessions, Map? outboundMessageSessions, Map? inboundKeySessions, Map? outboundKeySessions, - bool? deviceKeysExist, Map? deviceKeysOwned, Map>? deviceKeys, Map? oneTimeKeysClaimed, @@ -105,6 +108,7 @@ class CryptoStore extends Equatable { deviceKeysOwned: deviceKeysOwned ?? this.deviceKeysOwned, oneTimeKeysClaimed: oneTimeKeysClaimed ?? this.oneTimeKeysClaimed, deviceKeysExist: deviceKeysExist ?? this.deviceKeysExist, + deviceKeyVerified: deviceKeyVerified ?? this.deviceKeyVerified, oneTimeKeysCounts: oneTimeKeysCounts ?? this.oneTimeKeysCounts, ); diff --git a/lib/store/events/actions.dart b/lib/store/events/actions.dart index 7d5c8756b..eae49bb1e 100644 --- a/lib/store/events/actions.dart +++ b/lib/store/events/actions.dart @@ -1,14 +1,10 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: - import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; import 'package:syphon/global/algos.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/global/print.dart'; import 'package:syphon/storage/index.dart'; @@ -225,6 +221,8 @@ ThunkAction fetchMessageEvents({ ThunkAction decryptEvents(Room room, Map json) { return (Store store) async { try { + final verified = store.state.cryptoStore.deviceKeyVerified; + // First past to decrypt encrypted events final List timelineEvents = json['timeline']['events']; @@ -242,7 +240,7 @@ ThunkAction decryptEvents(Room room, Map json) { } catch (error) { debugPrint('[decryptMessageEvent] $error'); - if (!sentKeyRequest) { + if (!sentKeyRequest && verified) { sentKeyRequest = true; debugPrint('[decryptMessageEvent] SENDING KEY REQUEST'); store.dispatch(sendKeyRequest( @@ -344,8 +342,7 @@ ThunkAction selectReply({ }) { return (Store store) async { final room = store.state.roomStore.rooms[roomId!]!; - final reply = message ?? Message(); - store.dispatch(SetRoom(room: room.copyWith(reply: reply))); + store.dispatch(SetRoom(room: room.copyWith(reply: message ?? Null))); }; } diff --git a/lib/store/events/ephemeral/m.read/model.dart b/lib/store/events/ephemeral/m.read/model.dart index 23d51e75f..8d848492e 100644 --- a/lib/store/events/ephemeral/m.read/model.dart +++ b/lib/store/events/ephemeral/m.read/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:json_annotation/json_annotation.dart'; part 'model.g.dart'; diff --git a/lib/store/events/messages/actions.dart b/lib/store/events/messages/actions.dart index 2bc7fb299..dd496f06a 100644 --- a/lib/store/events/messages/actions.dart +++ b/lib/store/events/messages/actions.dart @@ -1,15 +1,11 @@ -// Dart imports: import 'dart:math'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/store/alerts/actions.dart'; import 'package:syphon/store/crypto/actions.dart'; diff --git a/lib/store/events/messages/model.dart b/lib/store/events/messages/model.dart index 3249d322e..82b934353 100644 --- a/lib/store/events/messages/model.dart +++ b/lib/store/events/messages/model.dart @@ -43,8 +43,8 @@ class Message extends Event { type, sender, stateKey, - timestamp, content, + timestamp = 0, this.body, this.msgtype, this.format, diff --git a/lib/store/events/model.dart b/lib/store/events/model.dart index 782094f0a..fd0324709 100644 --- a/lib/store/events/model.dart +++ b/lib/store/events/model.dart @@ -1,6 +1,4 @@ -// Package imports: import 'package:json_annotation/json_annotation.dart'; -import 'package:syphon/global/algos.dart'; part 'model.g.dart'; @@ -12,7 +10,7 @@ class Event { final String? type; final String? sender; final String? stateKey; - final int? timestamp; + final int timestamp; @JsonKey(ignore: true) final dynamic content; @@ -28,7 +26,7 @@ class Event { this.sender, this.stateKey, this.content, - this.timestamp, + this.timestamp = 0, this.data, }); @@ -39,7 +37,7 @@ class Event { roomId, stateKey, content, - timestamp, + int? timestamp, data, }) => Event( @@ -67,7 +65,7 @@ class Event { type: json['type'] as String?, sender: json['sender'] as String?, stateKey: json['state_key'] as String?, - timestamp: json['origin_server_ts'] as int?, + timestamp: json['origin_server_ts'] as int? ?? 0, content: json['content'] as dynamic, data: data, ); diff --git a/lib/store/events/parsers.dart b/lib/store/events/parsers.dart index 11845ddf4..f3ea01806 100644 --- a/lib/store/events/parsers.dart +++ b/lib/store/events/parsers.dart @@ -38,7 +38,7 @@ Map parseMessages({ List existing = const [], }) { bool? limited; - int? lastUpdate = room.lastUpdate; + int lastUpdate = room.lastUpdate; final outbox = List.from(room.outbox); final messagesAll = List.from(existing); @@ -48,7 +48,7 @@ Map parseMessages({ ); // See if the newest message has a greater timestamp - if (messages.isNotEmpty && lastUpdate < messages[0].timestamp!) { + if (messages.isNotEmpty && lastUpdate < messages[0].timestamp) { lastUpdate = messages[0].timestamp; } @@ -99,7 +99,7 @@ Map parseMessages({ outbox: outbox, messageIds: messageIdsAll.toList(), limited: limited ?? room.limited, - lastUpdate: lastUpdate ?? room.lastUpdate, + lastUpdate: lastUpdate, encryptionEnabled: room.encryptionEnabled || hasEncrypted != null, ), }; diff --git a/lib/store/events/reactions/actions.dart b/lib/store/events/reactions/actions.dart index c7b2ebfa0..4c79cf3e6 100644 --- a/lib/store/events/reactions/actions.dart +++ b/lib/store/events/reactions/actions.dart @@ -1,16 +1,12 @@ -// Dart imports: import 'dart:math'; -// Flutter imports: import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/store/events/actions.dart'; import 'package:syphon/store/events/model.dart'; diff --git a/lib/store/events/redaction/model.dart b/lib/store/events/redaction/model.dart index 2e08d4ed9..536bfe1e4 100644 --- a/lib/store/events/redaction/model.dart +++ b/lib/store/events/redaction/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:json_annotation/json_annotation.dart'; import 'package:syphon/global/algos.dart'; import 'package:syphon/store/events/model.dart'; diff --git a/lib/store/events/reducer.dart b/lib/store/events/reducer.dart index 40d4a6a30..704283326 100644 --- a/lib/store/events/reducer.dart +++ b/lib/store/events/reducer.dart @@ -1,4 +1,3 @@ -// Project imports: import 'package:syphon/store/events/ephemeral/m.read/model.dart'; import 'package:syphon/store/events/reactions/model.dart'; import 'package:syphon/store/events/redaction/model.dart'; diff --git a/lib/store/events/selectors.dart b/lib/store/events/selectors.dart index 0b632c150..ecf240374 100644 --- a/lib/store/events/selectors.dart +++ b/lib/store/events/selectors.dart @@ -1,4 +1,3 @@ -// Project imports: import 'dart:async'; import 'package:syphon/global/libs/matrix/constants.dart'; @@ -117,7 +116,7 @@ Map replaceEdited(List messages) { // sort replacements so they replace each other in order // iterate through replacements and modify messages as needed O(M + M) - replacements.sort((b, a) => a.timestamp!.compareTo(b.timestamp!)); + replacements.sort((b, a) => a.timestamp.compareTo(b.timestamp)); for (Message messageEdited in replacements) { final messageIdOriginal = messageEdited.relatedEventId!; @@ -150,7 +149,7 @@ Message? latestMessage(List messages) { return messages.fold( messages[0], - (latest, msg) => msg.timestamp! > latest!.timestamp! ? msg : latest, + (latest, msg) => msg.timestamp > latest!.timestamp ? msg : latest, ); } @@ -163,10 +162,10 @@ List latestMessages(List messages) { return -1; } - if (a.timestamp! > b.timestamp!) { + if (a.timestamp > b.timestamp) { return -1; } - if (a.timestamp! < b.timestamp!) { + if (a.timestamp < b.timestamp) { return 1; } diff --git a/lib/store/events/selectors.test.dart b/lib/store/events/selectors.test.dart new file mode 100644 index 000000000..8c6d096a0 --- /dev/null +++ b/lib/store/events/selectors.test.dart @@ -0,0 +1,27 @@ +import 'package:syphon/store/events/messages/model.dart'; +import 'package:syphon/store/events/selectors.dart'; +import 'package:test/test.dart'; + +void main() { + group('Event Selectors]', () { + test('latestMessage - one message works', () { + final messageLatest = Message(timestamp: 2); + final result = latestMessage([messageLatest]); + + expect(result, equals(messageLatest)); + }); + + test('latestMessage - largest timestamp of 2 wins', () { + final messageLatest = Message(timestamp: 2); + final result = latestMessage([messageLatest, Message()]); + + expect(result, equals(messageLatest)); + }); + + test('latestMessage - empty list', () { + final result = latestMessage([]); + + expect(result, equals(null)); + }); + }); +} diff --git a/lib/store/events/state.dart b/lib/store/events/state.dart index 7ee87f37f..865804abc 100644 --- a/lib/store/events/state.dart +++ b/lib/store/events/state.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:syphon/store/events/ephemeral/m.read/model.dart'; diff --git a/lib/store/index.dart b/lib/store/index.dart index dc7af4719..adebddf4b 100644 --- a/lib/store/index.dart +++ b/lib/store/index.dart @@ -1,20 +1,18 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:redux/redux.dart'; import 'package:redux_persist/redux_persist.dart'; import 'package:redux_thunk/redux_thunk.dart'; import 'package:sembast/sembast.dart'; +import 'package:syphon/cache/middleware.dart'; import 'package:syphon/cache/storage.dart'; import 'package:syphon/global/print.dart'; import 'package:syphon/storage/index.dart'; import 'package:syphon/storage/middleware.dart'; import 'package:syphon/store/alerts/middleware.dart'; -// Project imports: import 'package:syphon/store/alerts/model.dart'; import 'package:syphon/store/auth/actions.dart'; import 'package:syphon/store/auth/reducer.dart'; @@ -36,8 +34,8 @@ import './auth/state.dart'; import './media/state.dart'; import './rooms/reducer.dart'; import './rooms/state.dart'; -import './search/state.dart'; import './search/reducer.dart'; +import './search/state.dart'; import './settings/reducer.dart'; import './settings/state.dart'; @@ -99,7 +97,7 @@ AppState appReducer(AppState state, action) => AppState( ); /// Initialize Store -/// - Hot redux state cache for top level data +/// - Hot redux state cache for top level data Future> initStore(Database? cache, Database? storage) async { var data; @@ -112,21 +110,7 @@ Future> initStore(Database? cache, Database? storage) async { final persistor = Persistor( storage: CacheStorage(cache: cache), serializer: CacheSerializer(cache: cache, preloaded: data), - shouldSave: (Store store, dynamic action) { - switch (action.runtimeType) { - case SetRoom: - case SetOlmAccount: - case SetOlmAccountBackup: - case SetDeviceKeysOwned: - case SetUser: - case ResetCrypto: - case ResetUser: - printInfo('[initStore] persistor saving from ${action.runtimeType}'); - return true; - default: - return false; - } - }, + shouldSave: cacheMiddleware, ); // Finally load persisted store diff --git a/lib/store/media/actions.dart b/lib/store/media/actions.dart index b0afb3c7d..abfb0aaf9 100644 --- a/lib/store/media/actions.dart +++ b/lib/store/media/actions.dart @@ -1,18 +1,13 @@ -// Dart imports: import 'dart:io'; import 'dart:typed_data'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: - import 'package:mime/mime.dart'; import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/storage/index.dart'; import 'package:syphon/store/alerts/actions.dart'; diff --git a/lib/store/media/reducer.dart b/lib/store/media/reducer.dart index f8bb4d2fe..00c322742 100644 --- a/lib/store/media/reducer.dart +++ b/lib/store/media/reducer.dart @@ -1,7 +1,5 @@ -// Dart imports: import 'dart:typed_data'; -// Project imports: import './actions.dart'; import './state.dart'; diff --git a/lib/store/media/state.dart b/lib/store/media/state.dart index 61e9c7019..ba6610d85 100644 --- a/lib/store/media/state.dart +++ b/lib/store/media/state.dart @@ -1,7 +1,5 @@ -// Dart imports: import 'dart:typed_data'; -// Package imports: import 'package:equatable/equatable.dart'; // @JsonSerializable(nullable: true, includeIfNull: true) diff --git a/lib/store/rooms/actions.dart b/lib/store/rooms/actions.dart index 2d70b6179..ab79037ee 100644 --- a/lib/store/rooms/actions.dart +++ b/lib/store/rooms/actions.dart @@ -1,13 +1,9 @@ -// Dart imports: import 'dart:async'; import 'dart:io'; -// Flutter imports: import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/foundation.dart'; -// Package imports: - import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; import 'package:syphon/global/libs/matrix/constants.dart'; @@ -20,7 +16,6 @@ import 'package:syphon/store/events/parsers.dart'; import 'package:syphon/store/events/receipts/storage.dart'; import 'package:syphon/store/events/storage.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/encryption.dart'; import 'package:syphon/global/libs/matrix/errors.dart'; import 'package:syphon/global/libs/matrix/index.dart'; @@ -426,10 +421,7 @@ ThunkAction createRoom({ directUser.userId!, currentUser.userId!, ] as List, - users: { - directUser.userId!: directUser, - currentUser.userId!: currentUser - } as Map, + users: {directUser.userId!: directUser, currentUser.userId!: currentUser} as Map, ); await store.dispatch(toggleDirectRoom(room: room, enabled: true)); @@ -571,7 +563,7 @@ ThunkAction markRoomsReadAll() { /// /// Fetch the direct rooms list and recalculate it without the /// given alias -ThunkAction toggleDirectRoom({Room? room, bool? enabled}) { +ThunkAction toggleDirectRoom({Room? room, bool enabled = false}) { return (Store store) async { try { store.dispatch(SetLoading(loading: true)); @@ -596,7 +588,7 @@ ThunkAction toggleDirectRoom({Room? room, bool? enabled}) { (userId) => userId != currentUser.userId, ); - if (otherUserId == null) { + if (otherUserId == null && enabled) { throw 'Cannot toggle room to direct without other users'; } @@ -604,7 +596,7 @@ ThunkAction toggleDirectRoom({Room? room, bool? enabled}) { Map directRoomUsers = data as Map; final usersDirectRooms = directRoomUsers[otherUserId] ?? []; - if (usersDirectRooms.isEmpty && enabled!) { + if (usersDirectRooms.isEmpty && enabled) { directRoomUsers[otherUserId] = [room.id]; } @@ -616,7 +608,7 @@ ThunkAction toggleDirectRoom({Room? room, bool? enabled}) { return MapEntry(userId, updatedRooms); } - if (enabled!) { + if (enabled) { updatedRooms.add(room.id); } else { updatedRooms.removeWhere((roomId) => roomId == room.id); @@ -753,8 +745,7 @@ ThunkAction joinRoom({Room? room}) { final rooms = store.state.roomStore.rooms; - final Room joinedRoom = - rooms.containsKey(room.id) ? rooms[room.id]! : Room(id: room.id); + final Room joinedRoom = rooms.containsKey(room.id) ? rooms[room.id]! : Room(id: room.id); store.dispatch(SetRoom(room: joinedRoom.copyWith(invite: false))); @@ -822,9 +813,7 @@ ThunkAction acceptRoom({required Room room}) { final rooms = store.state.roomStore.rooms; - final Room joinedRoom = - rooms.containsKey(room.id) ? rooms[room.id]! : Room(id: room.id); - + final Room joinedRoom = rooms.containsKey(room.id) ? rooms[room.id]! : Room(id: room.id); store.dispatch(SetRoom(room: joinedRoom.copyWith(invite: false))); store.dispatch(SetLoading(loading: true)); @@ -836,6 +825,7 @@ ThunkAction acceptRoom({required Room room}) { }; } +/// /// Remove Room /// /// Both leaves and forgets room @@ -844,12 +834,16 @@ ThunkAction removeRoom({Room? room}) { try { store.dispatch(SetLoading(loading: true)); + if (room!.direct) { + await store.dispatch(toggleDirectRoom(room: room, enabled: false)); + } + // submit a leave room request final leaveData = await MatrixApi.leaveRoom( protocol: store.state.authStore.protocol, accessToken: store.state.authStore.user.accessToken, homeserver: store.state.authStore.user.homeserver, - roomId: room!.id, + roomId: room.id, ); // remove the room locally if it's already been removed remotely @@ -874,15 +868,10 @@ ThunkAction removeRoom({Room? room}) { } } - await deleteRooms({room.id: room}); + store.dispatch(RemoveRoom(roomId: room.id)); } catch (error) { debugPrint('[removeRoom] $error'); } finally { - if (room!.direct) { - await store.dispatch(toggleDirectRoom(room: room, enabled: false)); - } - - await store.dispatch(RemoveRoom(roomId: room.id)); store.dispatch(SetLoading(loading: false)); } }; @@ -920,6 +909,7 @@ ThunkAction leaveRoom({Room? room}) { } throw deleteData['error']; } + store.dispatch(RemoveRoom(roomId: room.id)); } catch (error) { printError('[leaveRoom] $error'); diff --git a/lib/store/rooms/reducer.dart b/lib/store/rooms/reducer.dart index f6de99a3d..db4e0622f 100644 --- a/lib/store/rooms/reducer.dart +++ b/lib/store/rooms/reducer.dart @@ -1,4 +1,3 @@ -// Project imports: import './actions.dart'; import './room/model.dart'; import './state.dart'; diff --git a/lib/store/rooms/room/model.dart b/lib/store/rooms/room/model.dart index faf824923..828628da0 100644 --- a/lib/store/rooms/room/model.dart +++ b/lib/store/rooms/room/model.dart @@ -1,13 +1,10 @@ -// Dart imports: import 'dart:collection'; -// Package imports: import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:syphon/global/print.dart'; -// Project imports: import 'package:syphon/global/strings.dart'; import 'package:syphon/store/events/ephemeral/m.read/model.dart'; import 'package:syphon/store/events/model.dart'; @@ -15,6 +12,7 @@ import 'package:syphon/global/libs/matrix/constants.dart'; import 'package:syphon/store/events/messages/model.dart'; import 'package:syphon/store/events/reactions/model.dart'; import 'package:syphon/store/events/redaction/model.dart'; +import 'package:syphon/store/settings/chat-settings/model.dart'; import 'package:syphon/store/user/model.dart'; part 'model.g.dart'; @@ -171,10 +169,10 @@ class Room { bool? hidden, bool? archived, joinRule, - lastRead, - lastUpdate, - namePriority, - totalJoinedUsers, + int? lastRead, + int? lastUpdate, + int? namePriority, + int? totalJoinedUsers, guestEnabled, encryptionEnabled, userTyping, @@ -221,7 +219,7 @@ class Room { userTyping: userTyping ?? this.userTyping, usersTyping: usersTyping ?? this.usersTyping, draft: draft ?? this.draft, - reply: reply ?? this.reply, + reply: reply == Null ? null : reply ?? this.reply, outbox: outbox ?? this.outbox, messageIds: messageIds ?? this.messageIds, messagesNew: messagesNew ?? this.messagesNew, @@ -278,30 +276,26 @@ class Room { if (json['state'] != null) { final List stateEventsRaw = json['state']['events']; - stateEvents = - stateEventsRaw.map((event) => Event.fromMatrix(event)).toList(); + stateEvents = stateEventsRaw.map((event) => Event.fromMatrix(event)).toList(); } if (json['invite_state'] != null) { final List stateEventsRaw = json['invite_state']['events']; - stateEvents = - stateEventsRaw.map((event) => Event.fromMatrix(event)).toList(); + stateEvents = stateEventsRaw.map((event) => Event.fromMatrix(event)).toList(); invite = true; } if (json['ephemeral'] != null) { final List ephemeralEventsRaw = json['ephemeral']['events']; - ephemeralEvents = - ephemeralEventsRaw.map((event) => Event.fromMatrix(event)).toList(); + ephemeralEvents = ephemeralEventsRaw.map((event) => Event.fromMatrix(event)).toList(); } if (json['account_data'] != null) { final List accountEventsRaw = json['account_data']['events']; - accountEvents = - accountEventsRaw.map((event) => Event.fromMatrix(event)).toList(); + accountEvents = accountEventsRaw.map((event) => Event.fromMatrix(event)).toList(); } // Find state and message updates from timeline @@ -342,9 +336,7 @@ class Room { } } - return fromAccountData( - accountEvents, - ) + return fromAccountData(accountEvents) .fromStateEvents( invite: invite, limited: limited, @@ -398,6 +390,7 @@ class Room { required List events, List? reactions, List? redactions, + LastUpdateType lastUpdateType = LastUpdateType.Message, }) { String? name; String? avatarUri; @@ -406,7 +399,7 @@ class Room { bool? encryptionEnabled; bool direct = this.direct; int? lastUpdate = this.lastUpdate; - int? namePriority = this.namePriority != 4 ? this.namePriority : 4; + int namePriority = this.namePriority; final Map usersAdd = Map.from(usersNew); Set userIds = Set.from(this.userIds); @@ -414,12 +407,14 @@ class Room { events.forEach((event) { try { - final timestamp = event.timestamp ?? 0; - lastUpdate = timestamp > lastUpdate! ? event.timestamp : lastUpdate; + final timestamp = event.timestamp; + if (lastUpdateType == LastUpdateType.State) { + lastUpdate = timestamp > lastUpdate! ? timestamp : lastUpdate; + } switch (event.type) { case 'm.room.name': - if (namePriority! > 0) { + if (namePriority > 0) { namePriority = 1; name = event.content['name']; } @@ -433,13 +428,13 @@ class Room { break; case 'm.room.canonical_alias': - if (namePriority! > 2) { + if (namePriority > 2) { namePriority = 2; name = event.content['alias']; } break; case 'm.room.aliases': - if (namePriority! > 3) { + if (namePriority > 3) { namePriority = 3; name = event.content['aliases'][0]; } @@ -490,16 +485,13 @@ class Room { try { // checks to make sure someone didn't name the room after the authed user - final badRoomName = - name == currentUser.displayName || name == currentUser.userId; + final badRoomName = name == currentUser.displayName || name == currentUser.userId; // no name room check - if ((namePriority! > 3 && usersAdd.isNotEmpty && direct) || badRoomName) { + if ((namePriority > 3 && usersAdd.isNotEmpty && direct) || badRoomName) { // Filter out number of non current users to show preview of total final otherUsers = usersAdd.values.where( - (user) => - user.userId != currentUser.userId && - user.displayName != currentUser.displayName, + (user) => user.userId != currentUser.userId, ); if (otherUsers.isNotEmpty) { @@ -508,19 +500,25 @@ class Room { final hasMultipleUsers = otherUsers.length > 1; // set name and avi to first non user or that + total others - name = hasMultipleUsers - ? '${shownUser.displayName} and ${usersAdd.values.length - 1}' - : shownUser.displayName; + name = shownUser.displayName; + + if (name == currentUser.displayName) { + name = '${shownUser.displayName} (${shownUser.userId})'; + } + + if (hasMultipleUsers) { + name = '${shownUser.displayName} and ${usersAdd.values.length - 1} others'; + } // set avatar if one has not been assigned - if (avatarUri == null && - this.avatarUri == null && - otherUsers.length == 1) { + if (avatarUri == null && this.avatarUri == null && otherUsers.length == 1) { avatarUri = shownUser.avatarUri; } } } - } catch (error) {} + } catch (error) { + printError('[directRoomName] ${error.toString()}'); + } return copyWith( name: name ?? this.name ?? Strings.labelRoomNameDefault, @@ -532,7 +530,7 @@ class Room { userIds: userIds.toList(), avatarUri: avatarUri ?? this.avatarUri, joinRule: joinRule ?? this.joinRule, - lastUpdate: lastUpdate! > 0 ? lastUpdate : this.lastUpdate, + lastUpdate: lastUpdate, encryptionEnabled: encryptionEnabled ?? this.encryptionEnabled, namePriority: namePriority, reactions: reactions, @@ -553,7 +551,7 @@ class Room { }) { try { bool? limited; - int? lastUpdate = this.lastUpdate; + int lastUpdate = this.lastUpdate; final messageIds = this.messageIds; // Converting only message events @@ -562,8 +560,12 @@ class Room { ); // See if the newest message has a greater timestamp - if (messages.isNotEmpty && lastUpdate < messages[0].timestamp!) { - lastUpdate = messages[0].timestamp; + final latestMessage = messages.firstWhereOrNull( + (msg) => lastUpdate < msg.timestamp, + ); + + if (latestMessage != null) { + lastUpdate = latestMessage.timestamp; } // limited indicates need to fetch additional data for room timelines @@ -582,9 +584,7 @@ class Room { // - the oldest hash (lastHash) is non-existant // - the previous hash (most recent) is non-existant // - the oldest hash equals the previously fetched hash - if (this.lastHash == null || - this.prevHash == null || - this.lastHash == this.prevHash) { + if (this.lastHash == null || this.prevHash == null || this.lastHash == this.prevHash) { limited = false; } } @@ -599,8 +599,7 @@ class Room { // save messages and unique message id updates final messageIdsNew = Set.from(messagesMap.keys); final messagesNew = List.from(messagesMap.values); - final messageIdsAll = Set.from(this.messageIds) - ..addAll(messageIdsNew); + final messageIdsAll = Set.from(messageIds)..addAll(messageIdsNew); // Save values to room return copyWith( @@ -608,7 +607,7 @@ class Room { messageIds: messageIdsAll.toList(), limited: limited ?? this.limited, encryptionEnabled: encryptionEnabled || hasEncrypted != null, - lastUpdate: lastUpdate ?? this.lastUpdate, + lastUpdate: lastUpdate, // oldest hash in the timeline lastHash: lastHash ?? this.lastHash ?? prevHash, // most recent prev_batch from the last /sync @@ -662,9 +661,7 @@ class Room { readReceipts[key] = readReceiptsNew; } else { // otherwise, add the usersRead to the existing reads - readReceipts[key]! - .userReads! - .addAll(readReceiptsNew.userReads!); + readReceipts[key]!.userReads!.addAll(readReceiptsNew.userReads!); } }); break; diff --git a/lib/store/rooms/room/selectors.dart b/lib/store/rooms/room/selectors.dart index 50c1392a5..027049278 100644 --- a/lib/store/rooms/room/selectors.dart +++ b/lib/store/rooms/room/selectors.dart @@ -1,4 +1,4 @@ -// Project imports: +import 'package:syphon/global/formatters.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/global/libs/matrix/constants.dart'; import 'package:syphon/store/events/messages/model.dart'; @@ -8,14 +8,26 @@ List availableRooms(List rooms) { return List.from(rooms.where((room) => !room.hidden)); } +String formatRoomName({required Room room}) { + final name = room.name!; + return name.length > 22 ? '${name.substring(0, 22)}...' : name; +} + +String formatRoomInitials({required Room room}) { + if (room.name == null || room.name!.isEmpty) { + return ''; + } + return formatInitialsLong(room.name); +} + String formatPreviewTopic(String? fullTopic) { final topic = fullTopic ?? Strings.contentTopicEmpty; final topicTruncated = topic.length > 100 ? topic.substring(0, 100) : topic; return topicTruncated.replaceAll('\n', ' '); } -String formatPreviewMessage(String body) { - return body.replaceAll('\n', ' '); +String formatPreviewMessage(String? body) { + return (body ?? '').replaceAll('\n', ' '); } String formatTotalUsers(int totalUsers) { @@ -25,7 +37,7 @@ String formatTotalUsers(int totalUsers) { String formatPreview({required Room room, Message? message}) { // Prioritize drafts for any room, regardless of state if (room.draft != null && room.draft!.body != null) { - return 'Draft: ${formatPreviewMessage(room.draft!.body!)}'; + return 'Draft: ${formatPreviewMessage(room.draft!.body)}'; } // Show topic if the user has joined a group but not sent @@ -37,7 +49,7 @@ String formatPreview({required Room room, Message? message}) { // room was created, but no messages or topic if (room.topic == null || room.topic!.isEmpty) { - return 'No messages yet'; + return 'No messages'; } // show the topic as the preview message @@ -45,24 +57,14 @@ String formatPreview({required Room room, Message? message}) { } // message was deleted - if (message.type != EventTypes.encrypted && - (message.body == '' || message.body == null)) { + if (message.type != EventTypes.encrypted && (message.body == null || message.body!.isEmpty)) { return 'This message was deleted'; } // message hasn't been decrypted - if (message.type == EventTypes.encrypted && message.body!.isEmpty) { + if (message.type == EventTypes.encrypted && (message.body == null || message.body!.isEmpty)) { return Strings.contentEncryptedMessage; } - return formatPreviewMessage(message.body!); -} - -String formatRoomName({required Room room}) { - final name = room.name!; - return name.length > 22 ? '${name.substring(0, 22)}...' : name; -} - -String formatRoomInitials({required Room room}) { - return room.name!.substring(0, 2).toUpperCase(); + return formatPreviewMessage(message.body); } diff --git a/lib/store/rooms/room/selectors.test.dart b/lib/store/rooms/room/selectors.test.dart new file mode 100644 index 000000000..98463fff4 --- /dev/null +++ b/lib/store/rooms/room/selectors.test.dart @@ -0,0 +1,29 @@ +import 'package:syphon/store/rooms/room/model.dart'; +import 'package:syphon/store/rooms/room/selectors.dart'; +import 'package:test/test.dart'; + +void main() { + group('[Room Selectors] ', () { + test('formatPreview - topic null', () { + final previewRoom = Room(id: '1234', topic: null); + + final previewText = formatPreview(room: previewRoom); + + expect('No messages', equals(previewText)); + }); + test('formatPreview - topic empty', () { + final previewRoom = Room(id: '1234', topic: ''); + + final previewText = formatPreview(room: previewRoom); + + expect('No messages', equals(previewText)); + }); + test('formatPreview - message null', () { + final previewRoom = Room(id: '1234', topic: ''); + + final previewText = formatPreview(room: previewRoom, message: null); + + expect('No messages', equals(previewText)); + }); + }); +} diff --git a/lib/store/rooms/selectors.dart b/lib/store/rooms/selectors.dart index 31ec33f1c..a80de3ad1 100644 --- a/lib/store/rooms/selectors.dart +++ b/lib/store/rooms/selectors.dart @@ -1,4 +1,3 @@ -// Project imports: import 'package:syphon/store/index.dart'; import './room/model.dart'; diff --git a/lib/store/rooms/state.dart b/lib/store/rooms/state.dart index 1d308189d..a3335a645 100644 --- a/lib/store/rooms/state.dart +++ b/lib/store/rooms/state.dart @@ -1,7 +1,5 @@ -// Dart imports: import 'dart:async'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/store/rooms/storage.dart b/lib/store/rooms/storage.dart index 5072ea41d..4bbb3c7de 100644 --- a/lib/store/rooms/storage.dart +++ b/lib/store/rooms/storage.dart @@ -45,7 +45,7 @@ Future deleteRooms( storage = storage ?? Storage.main; return storage!.transaction((txn) async { - for (Room? room in rooms.values) { + for (final Room? room in rooms.values) { final record = store.record(room?.id); await record.delete(txn); } diff --git a/lib/store/search/actions.dart b/lib/store/search/actions.dart index 84ef845e1..48bfbcb3a 100644 --- a/lib/store/search/actions.dart +++ b/lib/store/search/actions.dart @@ -1,18 +1,13 @@ -// Dart imports: import 'dart:async'; import 'package:http/http.dart' as http; -// Flutter imports: import 'package:flutter/material.dart'; -// Package imports: - import 'package:html/parser.dart'; import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; import 'package:syphon/global/libs/jack/index.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/global/print.dart'; import 'package:syphon/global/values.dart'; diff --git a/lib/store/search/reducer.dart b/lib/store/search/reducer.dart index c4bfff18d..7ad498930 100644 --- a/lib/store/search/reducer.dart +++ b/lib/store/search/reducer.dart @@ -1,4 +1,3 @@ -// Project imports: import './actions.dart'; import './state.dart'; diff --git a/lib/store/search/selectors.dart b/lib/store/search/selectors.dart index a1c3813b0..d78b2cd53 100644 --- a/lib/store/search/selectors.dart +++ b/lib/store/search/selectors.dart @@ -1,4 +1,3 @@ -// Project imports: import 'package:syphon/store/index.dart'; List homeservers(AppState state) { diff --git a/lib/store/search/state.dart b/lib/store/search/state.dart index 3f0a9e2c7..ef92bb682 100644 --- a/lib/store/search/state.dart +++ b/lib/store/search/state.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:syphon/store/auth/homeserver/model.dart'; diff --git a/lib/store/settings/actions.dart b/lib/store/settings/actions.dart index 91e2781c1..450270afb 100644 --- a/lib/store/settings/actions.dart +++ b/lib/store/settings/actions.dart @@ -1,12 +1,8 @@ -// Flutter imports: import 'package:flutter/material.dart'; -// Package imports: - import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/auth.dart'; import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/global/notifications.dart'; diff --git a/lib/store/settings/chat-settings/actions.dart b/lib/store/settings/chat-settings/actions.dart index cf5f78051..bd6987b8d 100644 --- a/lib/store/settings/chat-settings/actions.dart +++ b/lib/store/settings/chat-settings/actions.dart @@ -1,8 +1,6 @@ -// Package imports: import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/store/index.dart'; import 'package:syphon/store/settings/actions.dart'; diff --git a/lib/store/settings/chat-settings/model.dart b/lib/store/settings/chat-settings/model.dart index 89f308e98..a79fd762c 100644 --- a/lib/store/settings/chat-settings/model.dart +++ b/lib/store/settings/chat-settings/model.dart @@ -1,13 +1,16 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/store/settings/notification-settings/options/types.dart'; part 'model.g.dart'; +enum LastUpdateType { + Message, + State, +} + @JsonSerializable() class ChatSetting extends Equatable { final String roomId; diff --git a/lib/store/settings/chat-settings/selectors.dart b/lib/store/settings/chat-settings/selectors.dart new file mode 100644 index 000000000..174d8226b --- /dev/null +++ b/lib/store/settings/chat-settings/selectors.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:redux/redux.dart'; +import 'package:syphon/global/colours.dart'; +import 'package:syphon/store/index.dart'; + +/// +/// Chat Color per User +/// +Color selectChatColor(Store store, String? roomId) { + final chatSettings = store.state.settingsStore.chatSettings; + + if (chatSettings[roomId] == null) { + return Colours.hashedColor(roomId); + } + + return Color(chatSettings[roomId]!.primaryColor); +} + +/// +/// Chat Bubble Color per User +/// +Color? selectBubbleColor(Store store, String? roomId) { + final chatSettings = store.state.settingsStore.chatSettings; + + if (chatSettings[roomId] == null) { + return null; + } + + return Color(chatSettings[roomId]!.primaryColor); +} diff --git a/lib/store/settings/devices-settings/model.dart b/lib/store/settings/devices-settings/model.dart index 00c2c542b..f6f9080b1 100644 --- a/lib/store/settings/devices-settings/model.dart +++ b/lib/store/settings/devices-settings/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -7,14 +6,12 @@ part 'model.g.dart'; @JsonSerializable() class Device extends Equatable { final String? deviceId; - final String? deviceIdPrivate; final String? displayName; final String? lastSeenIp; final int? lastSeenTs; const Device({ this.deviceId, - this.deviceIdPrivate, this.displayName, this.lastSeenIp, this.lastSeenTs, @@ -23,7 +20,6 @@ class Device extends Equatable { @override List get props => [ deviceId, - deviceIdPrivate, displayName, lastSeenIp, lastSeenTs, @@ -31,14 +27,12 @@ class Device extends Equatable { Device copyWith({ String? deviceId, - String? deviceIdPrivate, String? displayName, String? lastSeenIp, int? lastSeenTs, }) => Device( deviceId: deviceId ?? this.deviceId, - deviceIdPrivate: deviceIdPrivate ?? this.deviceIdPrivate, displayName: displayName ?? this.displayName, lastSeenIp: lastSeenIp ?? this.lastSeenIp, lastSeenTs: lastSeenTs ?? this.lastSeenTs, diff --git a/lib/store/settings/notification-settings/actions.dart b/lib/store/settings/notification-settings/actions.dart index c5fc2ab1b..bf666ac5a 100644 --- a/lib/store/settings/notification-settings/actions.dart +++ b/lib/store/settings/notification-settings/actions.dart @@ -1,7 +1,6 @@ import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/store/index.dart'; import 'package:syphon/store/settings/actions.dart'; import 'package:syphon/store/settings/notification-settings/model.dart'; diff --git a/lib/store/settings/notification-settings/model.dart b/lib/store/settings/notification-settings/model.dart index aff9c5226..9ac9a5e95 100644 --- a/lib/store/settings/notification-settings/model.dart +++ b/lib/store/settings/notification-settings/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:syphon/store/settings/notification-settings/options/types.dart'; diff --git a/lib/store/settings/notification-settings/options/types.dart b/lib/store/settings/notification-settings/options/types.dart index 9733aa2c1..f590e23c7 100644 --- a/lib/store/settings/notification-settings/options/types.dart +++ b/lib/store/settings/notification-settings/options/types.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/store/settings/notification-settings/remote/actions.dart b/lib/store/settings/notification-settings/remote/actions.dart index 0e93a7796..1f81fcb49 100644 --- a/lib/store/settings/notification-settings/remote/actions.dart +++ b/lib/store/settings/notification-settings/remote/actions.dart @@ -1,12 +1,8 @@ -// Flutter imports: import 'package:flutter/material.dart'; -// Package imports: - import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/global/print.dart'; import 'package:syphon/global/values.dart'; diff --git a/lib/store/settings/notification-settings/remote/pushers/model.dart b/lib/store/settings/notification-settings/remote/pushers/model.dart index 30a311f55..8a9097bda 100644 --- a/lib/store/settings/notification-settings/remote/pushers/model.dart +++ b/lib/store/settings/notification-settings/remote/pushers/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/store/settings/notification-settings/remote/rules/model.dart b/lib/store/settings/notification-settings/remote/rules/model.dart index 8fcdc16df..81688c497 100644 --- a/lib/store/settings/notification-settings/remote/rules/model.dart +++ b/lib/store/settings/notification-settings/remote/rules/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/store/settings/reducer.dart b/lib/store/settings/reducer.dart index 5f3a75717..0eb5f9e9f 100644 --- a/lib/store/settings/reducer.dart +++ b/lib/store/settings/reducer.dart @@ -1,4 +1,3 @@ -// Project imports: import 'package:syphon/store/settings/chat-settings/model.dart'; import 'package:syphon/store/settings/notification-settings/actions.dart'; import './actions.dart'; diff --git a/lib/store/settings/state.dart b/lib/store/settings/state.dart index f700d3af5..ee712846b 100644 --- a/lib/store/settings/state.dart +++ b/lib/store/settings/state.dart @@ -1,8 +1,6 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -// Project imports: import 'package:syphon/global/themes.dart'; import 'package:syphon/global/colours.dart'; import 'package:syphon/store/settings/chat-settings/sort-order/model.dart'; diff --git a/lib/store/sync/actions.dart b/lib/store/sync/actions.dart index 2ac38c8c7..8014fd060 100644 --- a/lib/store/sync/actions.dart +++ b/lib/store/sync/actions.dart @@ -1,15 +1,10 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; -// Package imports: - import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; -// Project imports: import 'package:syphon/global/algos.dart'; import 'package:syphon/global/libs/matrix/errors.dart'; import 'package:syphon/global/libs/matrix/index.dart'; diff --git a/lib/store/sync/background/service.dart b/lib/store/sync/background/service.dart index 76ab979b5..bb6227a5b 100644 --- a/lib/store/sync/background/service.dart +++ b/lib/store/sync/background/service.dart @@ -1,24 +1,19 @@ -// Dart imports: import 'dart:math'; import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; -// Flutter imports: import 'package:flutter/material.dart'; -// Package imports: import 'package:android_alarm_manager/android_alarm_manager.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:syphon/cache/index.dart'; -// Package imports: import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:syphon/global/algos.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/global/notifications.dart'; import 'package:syphon/global/values.dart'; diff --git a/lib/store/sync/state.dart b/lib/store/sync/state.dart index a7c1406ae..90463d5e4 100644 --- a/lib/store/sync/state.dart +++ b/lib/store/sync/state.dart @@ -1,7 +1,5 @@ -// Dart imports: import 'dart:async'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/store/user/actions.dart b/lib/store/user/actions.dart index 8820fe701..10114a0e1 100644 --- a/lib/store/user/actions.dart +++ b/lib/store/user/actions.dart @@ -1,12 +1,9 @@ -// Package imports: - import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; import 'package:syphon/global/libs/matrix/errors.dart'; import 'package:syphon/global/libs/matrix/index.dart'; import 'package:syphon/store/alerts/actions.dart'; import 'package:syphon/store/index.dart'; -import 'package:syphon/store/events/model.dart'; import 'package:syphon/global/libs/matrix/constants.dart'; import 'package:syphon/store/user/model.dart'; @@ -97,7 +94,7 @@ ThunkAction fetchUser({User user = const User()}) { } /// Toggle Block User -/// +/// /// Fetch the blocked user list and recalculate /// events without the given user id ThunkAction toggleBlockUser({User? user = const User()}) { diff --git a/lib/store/user/model.dart b/lib/store/user/model.dart index 104bce17b..368d95389 100644 --- a/lib/store/user/model.dart +++ b/lib/store/user/model.dart @@ -1,4 +1,3 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/store/user/reducer.dart b/lib/store/user/reducer.dart index a58847ec1..fea4f5fcf 100644 --- a/lib/store/user/reducer.dart +++ b/lib/store/user/reducer.dart @@ -1,4 +1,3 @@ -// Project imports: import './actions.dart'; import './model.dart'; import './state.dart'; diff --git a/lib/store/user/selectors.dart b/lib/store/user/selectors.dart index c07c62660..c8aaf3d0d 100644 --- a/lib/store/user/selectors.dart +++ b/lib/store/user/selectors.dart @@ -1,4 +1,4 @@ -// Project imports: +import 'package:syphon/global/formatters.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/rooms/room/model.dart'; import './model.dart'; @@ -47,6 +47,7 @@ String trimAlias(String? alias) { } String formatAlias({String resource = '', String homeserver = ''}) { + // ignore: prefer_interpolation_to_compose_strings return '@' + resource + ':' + homeserver; } @@ -54,32 +55,12 @@ String formatUsername(User user) { return user.displayName ?? trimAlias(user.userId ?? ''); } -String formatInitials(String? fullword) { - // -> ? - if (fullword == null || fullword.isEmpty) { - return '?'; +String formatUserInitials(User? user) { + if (user == null || (user.displayName == null && user.userId == null)) { + return ''; } - // example words -> EW - if (fullword.contains(' ') && fullword.split(' ')[1].isNotEmpty) { - final words = fullword.split(' '); - final initialOne = words.elementAt(0).substring(0, 1); - final initialTwo = words.elementAt(1).substring(0, 1); - - return (initialOne + initialTwo).toUpperCase(); - } - - // example words -> EX - final word = fullword.replaceAll('@', ''); - - if (word.isEmpty) { - return 'NA'; - } - - final initials = - word.length > 1 ? word.substring(0, 2) : word.substring(0, 1); - - return initials.toUpperCase(); + return formatInitialsLong(user.displayName ?? user.userId); } List roomUsers(AppState state, String? roomId) { @@ -98,8 +79,6 @@ List searchUsersLocal( } return List.from(users.where( - (user) => - (user!.displayName ?? '').contains(searchText) || - (user.userId ?? '').contains(searchText), + (user) => (user!.displayName ?? '').contains(searchText) || (user.userId ?? '').contains(searchText), )); } diff --git a/lib/store/user/state.dart b/lib/store/user/state.dart index d5a9b7ccf..84f22746d 100644 --- a/lib/store/user/state.dart +++ b/lib/store/user/state.dart @@ -1,8 +1,6 @@ -// Package imports: import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -// Project imports: import 'package:syphon/store/user/model.dart'; part 'state.g.dart'; @@ -33,6 +31,7 @@ class UserStore extends Equatable { users, invites, loading, + blocked, ]; UserStore copyWith({ diff --git a/lib/views/behaviors.dart b/lib/views/behaviors.dart index db2896d32..7ad33ed2d 100644 --- a/lib/views/behaviors.dart +++ b/lib/views/behaviors.dart @@ -1,4 +1,3 @@ -// Flutter imports: import 'package:flutter/material.dart'; class DefaultScrollBehavior extends ScrollBehavior { diff --git a/lib/views/home/chat/details-all-users-screen.dart b/lib/views/home/chat/chat-detail-all-users-screen.dart similarity index 98% rename from lib/views/home/chat/details-all-users-screen.dart rename to lib/views/home/chat/chat-detail-all-users-screen.dart index 241d5f38a..48f03db6d 100644 --- a/lib/views/home/chat/details-all-users-screen.dart +++ b/lib/views/home/chat/chat-detail-all-users-screen.dart @@ -1,11 +1,8 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; @@ -16,7 +13,6 @@ import 'package:syphon/views/widgets/loader/index.dart'; import 'package:syphon/views/widgets/modals/modal-user-details.dart'; import 'package:touchable_opacity/touchable_opacity.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/home/chat/details-message-screen.dart b/lib/views/home/chat/chat-detail-message-screen.dart similarity index 98% rename from lib/views/home/chat/details-message-screen.dart rename to lib/views/home/chat/chat-detail-message-screen.dart index 50d3ea9d8..f826841ca 100644 --- a/lib/views/home/chat/details-message-screen.dart +++ b/lib/views/home/chat/chat-detail-message-screen.dart @@ -1,15 +1,12 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:intl/intl.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/themes.dart'; import 'package:syphon/store/index.dart'; @@ -60,9 +57,9 @@ class MessageDetailsScreen extends StatelessWidget { final Message message = props.message!; final timestamp = - DateTime.fromMillisecondsSinceEpoch(message.timestamp!); + DateTime.fromMillisecondsSinceEpoch(message.timestamp); final received = DateTime.fromMillisecondsSinceEpoch( - message.received ?? message.timestamp!); + message.received ?? message.timestamp); final isUserSent = props.userId == message.sender; diff --git a/lib/views/home/chat/details-chat-screen.dart b/lib/views/home/chat/chat-detail-screen.dart similarity index 80% rename from lib/views/home/chat/details-chat-screen.dart rename to lib/views/home/chat/chat-detail-screen.dart index 918a9d308..7df8769d7 100644 --- a/lib/views/home/chat/details-chat-screen.dart +++ b/lib/views/home/chat/chat-detail-screen.dart @@ -1,25 +1,23 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; +import 'package:syphon/global/colours.dart'; import 'package:syphon/global/strings.dart'; +import 'package:syphon/store/settings/chat-settings/selectors.dart'; import 'package:syphon/store/settings/notification-settings/actions.dart'; import 'package:syphon/store/settings/notification-settings/model.dart'; import 'package:syphon/store/settings/notification-settings/options/types.dart'; import 'package:syphon/store/user/actions.dart'; import 'package:syphon/store/user/selectors.dart'; -import 'package:syphon/views/home/chat/details-all-users-screen.dart'; +import 'package:syphon/views/home/chat/chat-detail-all-users-screen.dart'; import 'package:syphon/views/widgets/containers/card-section.dart'; import 'package:syphon/views/widgets/dialogs/dialog-confirm.dart'; import 'package:syphon/views/widgets/lists/list-user-bubbles.dart'; import 'package:touchable_opacity/touchable_opacity.dart'; -// Project imports: -import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/rooms/actions.dart'; @@ -28,17 +26,15 @@ import 'package:syphon/store/events/selectors.dart'; import 'package:syphon/store/rooms/room/model.dart'; import 'package:syphon/store/rooms/selectors.dart'; import 'package:syphon/store/settings/chat-settings/actions.dart'; -import 'package:syphon/store/settings/chat-settings/model.dart'; import 'package:syphon/store/user/model.dart'; import 'package:syphon/views/widgets/avatars/avatar.dart'; import 'package:syphon/views/widgets/dialogs/dialog-color-picker.dart'; -class ChatSettingsArguments { +class ChatDetailArguments { final String? roomId; final String? title; - // Improve loading times - ChatSettingsArguments({ + ChatDetailArguments({ this.roomId, this.title, }); @@ -52,7 +48,7 @@ class ChatDetailsScreen extends StatefulWidget { } class ChatDetailsState extends State { - ChatDetailsState({Key? key}) : super(); + ChatDetailsState() : super(); final ScrollController scrollController = ScrollController( initialScrollOffset: 0, @@ -100,8 +96,7 @@ class ChatDetailsState extends State { } @protected - Future onBlockUser( - {required BuildContext context, required _Props props}) async { + Future onBlockUser({required BuildContext context, required _Props props}) async { final user = props.users.firstWhere( (user) => user!.userId != props.currentUser.userId, ); @@ -151,13 +146,11 @@ class ChatDetailsState extends State { final titlePadding = Dimensions.listTitlePaddingDynamic(width: width); final contentPadding = Dimensions.listPaddingDynamic(width: width); - final ChatSettingsArguments? arguments = - ModalRoute.of(context)!.settings.arguments as ChatSettingsArguments?; + final ChatDetailArguments? arguments = ModalRoute.of(context)!.settings.arguments as ChatDetailArguments?; - final scaffordBackgroundColor = - Theme.of(context).brightness == Brightness.light - ? Colors.grey[200] - : Theme.of(context).scaffoldBackgroundColor; + final scaffordBackgroundColor = Theme.of(context).brightness == Brightness.light + ? Color(Colours.greyLightest) + : Theme.of(context).scaffoldBackgroundColor; return StoreConnector( distinct: true, @@ -166,8 +159,7 @@ class ChatDetailsState extends State { arguments?.roomId, ), builder: (context, props) { - var notificationsEnabled = - props.notificationSettings.toggleType == ToggleType.Enabled; + var notificationsEnabled = props.notificationSettings.toggleType == ToggleType.Enabled; if (props.notificationOptions != null) { notificationsEnabled = props.notificationOptions?.enabled ?? false; @@ -197,10 +189,7 @@ class ChatDetailsState extends State { child: Text( arguments!.title!, overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1! - .copyWith(color: Colors.white), + style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Colors.white), ), ), ], @@ -209,7 +198,7 @@ class ChatDetailsState extends State { tag: 'ChatAvatar', child: Container( padding: EdgeInsets.only(top: height * 0.075), - color: props.roomPrimaryColor, + color: props.chatColorPrimary, width: width, child: OverflowBox( minHeight: 64, @@ -223,7 +212,7 @@ class ChatDetailsState extends State { size: height * 0.15, uri: props.room.avatarUri, alt: props.room.name, - background: Colours.hashedColor(props.room.id), + background: props.chatColorPrimary, ), ), ], @@ -247,21 +236,16 @@ class ChatDetailsState extends State { width: width, padding: titlePadding, child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Container( - child: Row( - children: [ - Text( - Strings.labelUsersSection, - textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .subtitle2, - ), - ], - ), + Row( + children: [ + Text( + Strings.labelUsersSection, + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.subtitle2, + ), + ], ), TouchableOpacity( onTap: () { @@ -325,8 +309,7 @@ class ChatDetailsState extends State { Text( props.room.name!, textAlign: TextAlign.start, - style: - Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.headline6, ), Text( props.room.id, @@ -339,8 +322,7 @@ class ChatDetailsState extends State { style: Theme.of(context).textTheme.caption, ), Visibility( - visible: props.room.topic != null && - props.room.topic!.isNotEmpty, + visible: props.room.topic != null && props.room.topic!.isNotEmpty, child: Container( padding: EdgeInsets.only(top: 12), child: Text( @@ -378,13 +360,13 @@ class ChatDetailsState extends State { padding: EdgeInsets.only(right: 8), child: CircleAvatar( radius: 16, - backgroundColor: props.roomPrimaryColor, + backgroundColor: props.chatColorPrimary, ), ), onTap: () => onShowColorPicker( context: context, onSelectColor: props.onSelectPrimaryColor, - originalColor: props.roomPrimaryColor.value, + originalColor: props.chatColorPrimary.value, ), ), ListTile( @@ -428,8 +410,7 @@ class ChatDetailsState extends State { ), trailing: Switch( value: notificationsEnabled, - onChanged: (_) => - props.onToggleRoomNotifications(), + onChanged: (_) => props.onToggleRoomNotifications(), ), ), ListTile( @@ -442,10 +423,7 @@ class ChatDetailsState extends State { padding: EdgeInsets.symmetric(horizontal: 8), child: Text( 'Default', - style: Theme.of(context) - .textTheme - .subtitle1! - .copyWith(color: Colors.grey), + style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.grey), ), ), ), @@ -459,10 +437,7 @@ class ChatDetailsState extends State { padding: EdgeInsets.symmetric(horizontal: 8), child: Text( 'Default (Argon)', - style: Theme.of(context) - .textTheme - .subtitle1! - .copyWith(color: Colors.grey), + style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.grey), ), ), ), @@ -505,10 +480,7 @@ class ChatDetailsState extends State { contentPadding: contentPadding, title: Text( 'Block User', - style: Theme.of(context) - .textTheme - .subtitle1! - .copyWith( + style: Theme.of(context).textTheme.subtitle1!.copyWith( color: Colors.redAccent, ), ), @@ -519,10 +491,7 @@ class ChatDetailsState extends State { contentPadding: contentPadding, title: Text( 'Leave Chat', - style: Theme.of(context) - .textTheme - .subtitle1! - .copyWith( + style: Theme.of(context).textTheme.subtitle1!.copyWith( color: Colors.redAccent, ), ), @@ -548,7 +517,7 @@ class _Props extends Equatable { final bool loading; final User currentUser; final List users; - final Color roomPrimaryColor; + final Color chatColorPrimary; final List messages; final NotificationOptions? notificationOptions; final NotificationSettings notificationSettings; @@ -560,7 +529,7 @@ class _Props extends Equatable { final Function onToggleRoomNotifications; // final Function onViewEncryptionKeys; - _Props({ + const _Props({ required this.room, required this.users, required this.loading, @@ -568,7 +537,7 @@ class _Props extends Equatable { required this.currentUser, required this.onBlockUser, required this.onLeaveChat, - required this.roomPrimaryColor, + required this.chatColorPrimary, required this.onSelectPrimaryColor, required this.onToggleDirectRoom, required this.notificationOptions, @@ -581,50 +550,40 @@ class _Props extends Equatable { List get props => [ room, messages, - roomPrimaryColor, + chatColorPrimary, loading, ]; - static _Props mapStateToProps(Store store, String? roomId) => - _Props( - loading: store.state.roomStore.loading, - notificationSettings: store.state.settingsStore.notificationSettings, - notificationOptions: store.state.settingsStore.notificationSettings - .notificationOptions[roomId], - room: selectRoom(id: roomId, state: store.state), - users: roomUsers(store.state, roomId), - currentUser: store.state.authStore.user, - messages: roomMessages(store.state, roomId), - onToggleRoomNotifications: () async { - if (roomId != null) { - await store.dispatch(toggleChatNotifications(roomId: roomId)); - } - }, - onBlockUser: (User user) async { - await store.dispatch(toggleBlockUser(user: user)); - }, - onLeaveChat: () async { - await store.dispatch(removeRoom( - room: selectRoom(state: store.state, id: roomId), - )); - }, - roomPrimaryColor: () { - final chatSettings = store.state.settingsStore.chatSettings; - - if (chatSettings[roomId] == null) { - return Colours.hashedColor(roomId); - } - - return Color(chatSettings[roomId]!.primaryColor); - }(), - onSelectPrimaryColor: (color) { - store.dispatch(updateRoomPrimaryColor( - roomId: roomId, - color: color, - )); - }, - onToggleDirectRoom: () { - final room = selectRoom(id: roomId, state: store.state); - store.dispatch(toggleDirectRoom(room: room, enabled: !room.direct)); - }); + static _Props mapStateToProps(Store store, String? roomId) => _Props( + loading: store.state.roomStore.loading, + notificationSettings: store.state.settingsStore.notificationSettings, + notificationOptions: store.state.settingsStore.notificationSettings.notificationOptions[roomId], + room: selectRoom(id: roomId, state: store.state), + users: roomUsers(store.state, roomId), + currentUser: store.state.authStore.user, + messages: roomMessages(store.state, roomId), + onToggleRoomNotifications: () async { + if (roomId != null) { + await store.dispatch(toggleChatNotifications(roomId: roomId)); + } + }, + onBlockUser: (User user) async { + await store.dispatch(toggleBlockUser(user: user)); + }, + onLeaveChat: () async { + await store.dispatch(leaveRoom( + room: selectRoom(state: store.state, id: roomId), + )); + }, + chatColorPrimary: selectChatColor(store, roomId), + onSelectPrimaryColor: (color) { + store.dispatch(updateRoomPrimaryColor( + roomId: roomId, + color: color, + )); + }, + onToggleDirectRoom: () { + final room = selectRoom(id: roomId, state: store.state); + store.dispatch(toggleDirectRoom(room: room, enabled: !room.direct)); + }); } diff --git a/lib/views/home/chat/chat-screen.dart b/lib/views/home/chat/chat-screen.dart index a1824aa03..6b4db3f09 100644 --- a/lib/views/home/chat/chat-screen.dart +++ b/lib/views/home/chat/chat-screen.dart @@ -1,21 +1,16 @@ -// Dart imports: import 'dart:io'; -// Flutter imports: ; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/svg.dart'; import 'package:redux/redux.dart'; -import 'package:syphon/global/algos.dart'; import 'package:syphon/global/assets.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/print.dart'; @@ -31,6 +26,7 @@ import 'package:syphon/global/libs/matrix/constants.dart'; import 'package:syphon/store/events/messages/model.dart'; import 'package:syphon/store/rooms/room/model.dart'; import 'package:syphon/store/rooms/selectors.dart'; +import 'package:syphon/store/settings/chat-settings/selectors.dart'; import 'package:syphon/views/home/chat/widgets/chat-input.dart'; import 'package:syphon/views/home/chat/widgets/dialog-encryption.dart'; import 'package:syphon/views/home/chat/widgets/dialog-invite.dart'; @@ -89,6 +85,10 @@ class ChatScreenState extends State { barrierDismissible: false, builder: (context) => DialogInvite( onAccept: props.onAcceptInvite, + onReject: () { + props.onRejectInvite(); + Navigator.popUntil(context, (route) => route.isFirst); + }, onCancel: () { Navigator.popUntil(context, (route) => route.isFirst); }, @@ -272,10 +272,9 @@ class ChatScreenState extends State { ); } - @protected onShowMediumMenu(context, _Props props) async { - double width = MediaQuery.of(context).size.width; - double height = MediaQuery.of(context).size.height; + final double width = MediaQuery.of(context).size.width; + final double height = MediaQuery.of(context).size.height; showMenu( elevation: 4.0, @@ -296,7 +295,7 @@ class ChatScreenState extends State { child: GestureDetector( onTap: () { Navigator.pop(context); - this.onChangeMediumType( + onChangeMediumType( newMediumType: MediumType.plaintext, props: props, ); @@ -329,7 +328,7 @@ class ChatScreenState extends State { ? null : () { Navigator.pop(context); - this.onChangeMediumType( + onChangeMediumType( newMediumType: MediumType.encryption, props: props, ); @@ -366,18 +365,15 @@ class ChatScreenState extends State { onInitialBuild: onMounted, converter: (Store store) => _Props.mapStateToProps( store, - (ModalRoute.of(context)!.settings.arguments as ChatViewArguements) - .roomId, + (ModalRoute.of(context)!.settings.arguments as ChatViewArguements).roomId, ), builder: (context, props) { - double height = MediaQuery.of(context).size.height; + final double height = MediaQuery.of(context).size.height; - final closedInputPadding = !inputFieldNode.hasFocus && - Platform.isIOS && - Dimensions.buttonlessHeightiOS < height; + final closedInputPadding = + !inputFieldNode.hasFocus && Platform.isIOS && Dimensions.buttonlessHeightiOS < height; - final isScrolling = - messagesController.hasClients && messagesController.offset != 0; + final isScrolling = messagesController.hasClients && messagesController.offset != 0; Color inputContainerColor = Colors.white; @@ -387,7 +383,7 @@ class ChatScreenState extends State { Widget appBar = AppBarChat( room: props.room, - color: props.roomPrimaryColor, + color: props.chatColorPrimary, badgesEnabled: props.roomTypeBadgesEnabled, onDebug: () { props.onCheatCode(); @@ -406,13 +402,13 @@ class ChatScreenState extends State { }, ); - if (this.selectedMessage != null) { + if (selectedMessage != null) { appBar = AppBarMessageOptions( room: props.room, message: selectedMessage, onDismiss: () => onToggleSelectedMessage(null), onDelete: () => props.onDeleteMessage( - message: this.selectedMessage, + message: selectedMessage, ), ); } @@ -462,9 +458,7 @@ class ChatScreenState extends State { children: [ Text( 'Load more messages', - style: Theme.of(context) - .textTheme - .bodyText2, + style: Theme.of(context).textTheme.bodyText2, ) ], ), @@ -486,17 +480,11 @@ class ChatScreenState extends State { decoration: BoxDecoration( color: inputContainerColor, boxShadow: isScrolling - ? [ - BoxShadow( - blurRadius: 6, - offset: Offset(0, -4), - color: Colors.black12) - ] + ? [BoxShadow(blurRadius: 6, offset: Offset(0, -4), color: Colors.black12)] : [], ), child: AnimatedPadding( - duration: Duration( - milliseconds: inputFieldNode.hasFocus ? 225 : 0), + duration: Duration(milliseconds: inputFieldNode.hasFocus ? 225 : 0), padding: EdgeInsets.only( bottom: closedInputPadding ? 16 : 0, ), @@ -511,7 +499,6 @@ class ChatScreenState extends State { onCancelReply: () => props.onSelectReply(null), onChangeMethod: () => onShowMediumMenu(context, props), onSubmitMessage: () => onSubmitMessage(props), - onSubmittedMessage: (text) => onSubmitMessage(props), ), ), ), @@ -530,7 +517,7 @@ class _Props extends Equatable { final int? messagesLength; final bool enterSendEnabled; final ThemeType theme; - final Color roomPrimaryColor; + final Color chatColorPrimary; final bool roomTypeBadgesEnabled; final bool dismissKeyboardEnabled; @@ -542,20 +529,21 @@ class _Props extends Equatable { final Function onLoadMoreMessages; final Function onLoadFirstBatch; final Function onAcceptInvite; + final Function onRejectInvite; final Function onToggleEncryption; final Function onToggleReaction; final Function onCheatCode; final Function onMarkRead; final Function onSelectReply; - _Props({ + const _Props({ required this.room, required this.theme, required this.userId, required this.loading, required this.messagesLength, required this.enterSendEnabled, - required this.roomPrimaryColor, + required this.chatColorPrimary, required this.roomTypeBadgesEnabled, required this.dismissKeyboardEnabled, required this.onUpdateDeviceKeys, @@ -566,6 +554,7 @@ class _Props extends Equatable { required this.onLoadMoreMessages, required this.onLoadFirstBatch, required this.onAcceptInvite, + required this.onRejectInvite, required this.onToggleEncryption, required this.onToggleReaction, required this.onCheatCode, @@ -579,158 +568,152 @@ class _Props extends Equatable { userId, loading, enterSendEnabled, - roomPrimaryColor, + chatColorPrimary, ]; - static _Props mapStateToProps(Store store, String? roomId) => - _Props( - room: selectRoom(id: roomId, state: store.state), - theme: store.state.settingsStore.theme, - userId: store.state.authStore.user.userId, - roomTypeBadgesEnabled: - store.state.settingsStore.roomTypeBadgesEnabled, - dismissKeyboardEnabled: - store.state.settingsStore.dismissKeyboardEnabled, - enterSendEnabled: store.state.settingsStore.enterSendEnabled, - loading: selectRoom(state: store.state, id: roomId).syncing, - messagesLength: store.state.eventStore.messages.containsKey(roomId) - ? store.state.eventStore.messages[roomId]?.length - : 0, - onSelectReply: (Message message) { - store.dispatch(selectReply(roomId: roomId, message: message)); - }, - roomPrimaryColor: () { - final chatSettings = store.state.settingsStore.chatSettings; - - if (chatSettings[roomId] == null) { - return Colours.hashedColor(roomId); - } - - return Color(chatSettings[roomId]!.primaryColor); - }(), - onUpdateDeviceKeys: () async { - final room = store.state.roomStore.rooms[roomId]!; - - final usersDeviceKeys = await store.dispatch( - fetchDeviceKeys(userIds: room.userIds), - ); - - store.dispatch(setDeviceKeys(usersDeviceKeys)); - }, - onSaveDraftMessage: ({ - String? body, - String? type, - }) { - store.dispatch(saveDraft( - body: body, - type: type, - room: store.state.roomStore.rooms[roomId], - )); - }, - onClearDraftMessage: ({ - String? body, - String? type, - }) { - store.dispatch(clearDraft( - room: store.state.roomStore.rooms[roomId], - )); - }, - onSendMessage: ({required String body, String? type}) async { - if (roomId == null || body.isEmpty) return; - - final room = store.state.roomStore.rooms[roomId]!; - - final message = Message( - body: body, - type: type, - ); - - if (room.encryptionEnabled) { - return store.dispatch(sendMessageEncrypted( - roomId: roomId, - message: message, - )); - } - - return store.dispatch(sendMessage( - room: room, - message: message, - )); - }, - onDeleteMessage: ({ - Message? message, - }) { - if (message != null) { - store.dispatch(deleteMessage(message: message)); - } - }, - onAcceptInvite: () { - store.dispatch(acceptRoom( - room: selectRoom(state: store.state, id: roomId), - )); - }, - onMarkRead: () { - store.dispatch(markRoomRead(roomId: roomId)); - }, - onLoadFirstBatch: () { - final room = selectRoom(id: roomId, state: store.state); - - store.dispatch(fetchMessageEvents( - room: room, - from: room.nextHash, - limit: 25, - )); - }, - onToggleReaction: ({Message? message, String? emoji}) { - final room = selectRoom(id: roomId, state: store.state); - - store.dispatch( - toggleReaction(room: room, message: message, emoji: emoji), - ); - }, - onToggleEncryption: () { - final room = selectRoom(id: roomId, state: store.state); - store.dispatch( - toggleRoomEncryption(room: room), - ); - }, - onLoadMoreMessages: () { - final room = selectRoom(state: store.state, id: roomId); - - // load message from cold storage - // TODO: paginate cold storage messages - // final messages = roomMessages(store.state, roomId); - // if (messages.length < room.messageIds.length) { - // printDebug( - // '[onLoadMoreMessages] loading from cold storage ${messages.length} ${room.messageIds.length}'); - // return store.dispatch( - // loadMessageEvents( - // room: room, - // offset: messages.length, - // ), - // ); - // } - - // fetch messages beyond the oldest known message - lastHash - return store.dispatch(fetchMessageEvents( - room: room, - from: room.lastHash, - oldest: true, - )); - }, - onCheatCode: () async { - // await store.dispatch(store.dispatch(generateDeviceId( - // salt: store.state.authStore.username, - // ))); - - final room = selectRoom(state: store.state, id: roomId); + static _Props mapStateToProps(Store store, String? roomId) => _Props( + room: selectRoom(id: roomId, state: store.state), + theme: store.state.settingsStore.theme, + userId: store.state.authStore.user.userId, + roomTypeBadgesEnabled: store.state.settingsStore.roomTypeBadgesEnabled, + dismissKeyboardEnabled: store.state.settingsStore.dismissKeyboardEnabled, + enterSendEnabled: store.state.settingsStore.enterSendEnabled, + loading: selectRoom(state: store.state, id: roomId).syncing, + messagesLength: store.state.eventStore.messages.containsKey(roomId) + ? store.state.eventStore.messages[roomId]?.length + : 0, + onSelectReply: (Message? message) { + store.dispatch(selectReply(roomId: roomId, message: message)); + }, + chatColorPrimary: selectChatColor(store, roomId), + onUpdateDeviceKeys: () async { + final room = store.state.roomStore.rooms[roomId]!; + + final usersDeviceKeys = await store.dispatch( + fetchDeviceKeys(userIds: room.userIds), + ); - store.dispatch(updateKeySessions(room: room)); + store.dispatch(setDeviceKeys(usersDeviceKeys)); + }, + onSaveDraftMessage: ({ + String? body, + String? type, + }) { + store.dispatch(saveDraft( + body: body, + type: type, + room: store.state.roomStore.rooms[roomId], + )); + }, + onClearDraftMessage: ({ + String? body, + String? type, + }) { + store.dispatch(clearDraft( + room: store.state.roomStore.rooms[roomId], + )); + }, + onSendMessage: ({required String body, String? type}) async { + if (roomId == null || body.isEmpty) return; + + final room = store.state.roomStore.rooms[roomId]!; + + final message = Message( + body: body, + type: type, + ); - final usersDeviceKeys = await store.dispatch( - fetchDeviceKeys(userIds: room.userIds), - ); + if (room.encryptionEnabled) { + return store.dispatch(sendMessageEncrypted( + roomId: roomId, + message: message, + )); + } + + return store.dispatch(sendMessage( + room: room, + message: message, + )); + }, + onDeleteMessage: ({ + Message? message, + }) { + if (message != null) { + store.dispatch(deleteMessage(message: message)); + } + }, + onAcceptInvite: () { + store.dispatch(acceptRoom( + room: selectRoom(state: store.state, id: roomId), + )); + }, + onRejectInvite: () { + store.dispatch(leaveRoom( + room: selectRoom(state: store.state, id: roomId), + )); + }, + onMarkRead: () { + store.dispatch(markRoomRead(roomId: roomId)); + }, + onLoadFirstBatch: () { + final room = selectRoom(id: roomId, state: store.state); + + store.dispatch(fetchMessageEvents( + room: room, + from: room.nextHash, + limit: 25, + )); + }, + onToggleReaction: ({Message? message, String? emoji}) { + final room = selectRoom(id: roomId, state: store.state); + + store.dispatch( + toggleReaction(room: room, message: message, emoji: emoji), + ); + }, + onToggleEncryption: () { + final room = selectRoom(id: roomId, state: store.state); + store.dispatch( + toggleRoomEncryption(room: room), + ); + }, + onLoadMoreMessages: () { + final room = selectRoom(state: store.state, id: roomId); + + // load message from cold storage + // TODO: paginate cold storage messages + // final messages = roomMessages(store.state, roomId); + // if (messages.length < room.messageIds.length) { + // printDebug( + // '[onLoadMoreMessages] loading from cold storage ${messages.length} ${room.messageIds.length}'); + // return store.dispatch( + // loadMessageEvents( + // room: room, + // offset: messages.length, + // ), + // ); + // } + + // fetch messages beyond the oldest known message - lastHash + return store.dispatch(fetchMessageEvents( + room: room, + from: room.lastHash, + oldest: true, + )); + }, + onCheatCode: () async { + // await store.dispatch(store.dispatch(generateDeviceId( + // salt: store.state.authStore.username, + // ))); + + final room = selectRoom(state: store.state, id: roomId); + + store.dispatch(updateKeySessions(room: room)); + + final usersDeviceKeys = await store.dispatch( + fetchDeviceKeys(userIds: room.userIds), + ); - printJson(usersDeviceKeys); - }); + printJson(usersDeviceKeys); + }); } diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 44fd8a244..ae4e56c44 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -1,5 +1,5 @@ -// Flutter imports: import 'dart:async'; +import 'dart:io'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; @@ -9,7 +9,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:redux/redux.dart'; import 'package:syphon/global/assets.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; @@ -30,7 +29,6 @@ class ChatInput extends StatefulWidget { final TextEditingController controller; final Function? onSubmitMessage; - final Function? onSubmittedMessage; final Function? onChangeMethod; final Function? onUpdateMessage; final Function? onCancelReply; @@ -47,7 +45,6 @@ class ChatInput extends StatefulWidget { this.onUpdateMessage, this.onChangeMethod, this.onSubmitMessage, - this.onSubmittedMessage, this.onCancelReply, }) : super(key: key); @@ -97,7 +94,18 @@ class ChatInputState extends State { } } - @protected + @override + void dispose() { + super.dispose(); + if (typingNotifier != null) { + typingNotifier!.cancel(); + } + + if (typingNotifierTimeout != null) { + typingNotifierTimeout!.cancel(); + } + } + onUpdate(String text, {_Props? props}) { setState(() { sendable = text.trim().isNotEmpty; @@ -139,23 +147,26 @@ class ChatInputState extends State { } } - @override - void dispose() { - super.dispose(); - if (typingNotifier != null) { - typingNotifier!.cancel(); + onSubmit() { + setState(() { + sendable = false; + }); + + if (widget.onSubmitMessage != null) { + widget.onSubmitMessage!(); } + } - if (typingNotifierTimeout != null) { - typingNotifierTimeout!.cancel(); + onCancelReply() { + if (widget.onCancelReply != null) { + widget.onCancelReply!(); } } @override Widget build(BuildContext context) => StoreConnector( distinct: true, - converter: (Store store) => - _Props.mapStateToProps(store, widget.roomId), + converter: (Store store) => _Props.mapStateToProps(store, widget.roomId), onInitialBuild: onMounted, builder: (context, props) { final double width = MediaQuery.of(context).size.width; @@ -163,19 +174,20 @@ class ChatInputState extends State { // dynamic dimensions final double messageInputWidth = width - 72; - final bool replying = - widget.quotable != null && widget.quotable!.sender != null; + final bool replying = widget.quotable != null && widget.quotable!.sender != null; final double maxHeight = replying ? height * 0.45 : height * 0.5; final isSendable = sendable && !widget.sending; + + if (!isSendable) { + sendButtonColor = Color(Colours.greyDisabled); + } + if (widget.mediumType == MediumType.plaintext) { + hintText = Strings.placeholderInputMatrixUnencrypted; + if (isSendable) { - if (Theme.of(context).accentColor != - Theme.of(context).primaryColor) { - sendButtonColor = Theme.of(context).accentColor; - } else { - sendButtonColor = Colors.grey[700]; - } + sendButtonColor = Theme.of(context).accentColor; } } @@ -190,8 +202,7 @@ class ChatInputState extends State { var sendButton = InkWell( borderRadius: BorderRadius.circular(48), onLongPress: widget.onChangeMethod as void Function()?, - onTap: - !isSendable ? null : widget.onSubmitMessage as void Function()?, + onTap: !isSendable ? null : onSubmit, child: CircleAvatar( backgroundColor: sendButtonColor, child: Container( @@ -209,9 +220,7 @@ class ChatInputState extends State { sendButton = InkWell( borderRadius: BorderRadius.circular(48), onLongPress: widget.onChangeMethod as void Function()?, - onTap: !isSendable - ? null - : widget.onSubmitMessage as void Function()?, + onTap: !isSendable ? null : onSubmit, child: CircleAvatar( backgroundColor: sendButtonColor, child: Container( @@ -255,12 +264,9 @@ class ChatInputState extends State { ), decoration: InputDecoration( filled: true, - labelText: - replying ? widget.quotable!.sender : '', - labelStyle: TextStyle( - color: Theme.of(context).accentColor), - contentPadding: Dimensions.inputContentPadding - .copyWith(right: 36), + labelText: replying ? widget.quotable!.sender : '', + labelStyle: TextStyle(color: Theme.of(context).accentColor), + contentPadding: Dimensions.inputContentPadding.copyWith(right: 36), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).accentColor, @@ -269,10 +275,8 @@ class ChatInputState extends State { borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), - bottomLeft: - Radius.circular(!replying ? 24 : 0), - bottomRight: - Radius.circular(!replying ? 24 : 0), + bottomLeft: Radius.circular(!replying ? 24 : 0), + bottomRight: Radius.circular(!replying ? 24 : 0), ), ), border: OutlineInputBorder( @@ -283,10 +287,8 @@ class ChatInputState extends State { borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), - bottomLeft: - Radius.circular(!replying ? 24 : 0), - bottomRight: - Radius.circular(!replying ? 24 : 0), + bottomLeft: Radius.circular(!replying ? 24 : 0), + bottomRight: Radius.circular(!replying ? 24 : 0), ), ), ), @@ -297,7 +299,7 @@ class ChatInputState extends State { right: 0, bottom: 0, child: IconButton( - onPressed: () => widget.onCancelReply!(), + onPressed: () => onCancelReply(), icon: Icon( Icons.close, size: Dimensions.iconSize, @@ -321,19 +323,15 @@ class ChatInputState extends State { ), child: TextField( maxLines: null, - autocorrect: false, - enableSuggestions: false, + autocorrect: props.autocorrectEnabled, + enableSuggestions: props.suggestionsEnabled, keyboardType: TextInputType.multiline, - textInputAction: widget.enterSend - ? TextInputAction.send - : TextInputAction.newline, + textInputAction: widget.enterSend ? TextInputAction.send : TextInputAction.newline, cursorColor: inputCursorColor, focusNode: widget.focusNode, controller: widget.controller, onChanged: (text) => onUpdate(text, props: props), - onSubmitted: !isSendable - ? null - : widget.onSubmittedMessage as void Function(String)?, + onSubmitted: !isSendable ? null : (text) => onSubmit(), style: TextStyle( height: 1.5, color: inputTextColor, @@ -344,8 +342,7 @@ class ChatInputState extends State { fillColor: inputColorBackground, contentPadding: Dimensions.inputContentPadding, focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).accentColor, width: 1), + borderSide: BorderSide(color: Theme.of(context).accentColor, width: 1), borderRadius: BorderRadius.only( topLeft: Radius.circular(!replying ? 24 : 0), topRight: Radius.circular(!replying ? 24 : 0), @@ -378,12 +375,16 @@ class ChatInputState extends State { class _Props extends Equatable { final Room room; final bool enterSendEnabled; + final bool autocorrectEnabled; + final bool suggestionsEnabled; final Function onSendTyping; - _Props({ + const _Props({ required this.room, required this.enterSendEnabled, + required this.autocorrectEnabled, + required this.suggestionsEnabled, required this.onSendTyping, }); @@ -396,6 +397,8 @@ class _Props extends Equatable { static _Props mapStateToProps(Store store, String roomId) => _Props( room: selectRoom(id: roomId, state: store.state), enterSendEnabled: store.state.settingsStore.enterSendEnabled, + autocorrectEnabled: Platform.isIOS, // TODO: toggle-able setting + suggestionsEnabled: Platform.isIOS, // TODO: toggle-able setting onSendTyping: ({typing, roomId}) => store.dispatch( sendTyping(typing: typing, roomId: roomId), ), diff --git a/lib/views/home/chat/widgets/dialog-encryption.dart b/lib/views/home/chat/widgets/dialog-encryption.dart index 29add81df..f6c5e0aeb 100644 --- a/lib/views/home/chat/widgets/dialog-encryption.dart +++ b/lib/views/home/chat/widgets/dialog-encryption.dart @@ -1,7 +1,5 @@ -// Flutter imports: import 'package:flutter/material.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/home/chat/widgets/dialog-invite.dart b/lib/views/home/chat/widgets/dialog-invite.dart index 558afbefd..4bb2d1a05 100644 --- a/lib/views/home/chat/widgets/dialog-invite.dart +++ b/lib/views/home/chat/widgets/dialog-invite.dart @@ -1,18 +1,18 @@ -// Flutter imports: import 'package:flutter/material.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; class DialogInvite extends StatelessWidget { - DialogInvite({ + const DialogInvite({ Key? key, this.onAccept, + this.onReject, this.onCancel, }) : super(key: key); final Function? onAccept; + final Function? onReject; final Function? onCancel; @override @@ -24,15 +24,30 @@ class DialogInvite extends StatelessWidget { contentPadding: Dimensions.dialogPadding, children: [ Container( + padding: Dimensions.dialogContentPadding, child: Text( Strings.confirmationAcceptInvite, textAlign: TextAlign.left, ), - padding: Dimensions.dialogContentPadding, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + SimpleDialogOption( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + onPressed: () { + if (onCancel != null) { + onCancel!(); + } + }, + child: Text( + 'go back', + style: Theme.of(context).textTheme.subtitle1, + ), + ), Spacer(flex: 1), Row( mainAxisAlignment: MainAxisAlignment.end, @@ -43,12 +58,12 @@ class DialogInvite extends StatelessWidget { vertical: 12, ), onPressed: () { - if (onCancel != null) { - onCancel!(); + if (onReject != null) { + onReject!(); } }, child: Text( - 'go back', + 'reject', style: Theme.of(context).textTheme.subtitle1, ), ), diff --git a/lib/views/home/chat/widgets/message-list.dart b/lib/views/home/chat/widgets/message-list.dart index 4fe2e7041..9fdf4f3b8 100644 --- a/lib/views/home/chat/widgets/message-list.dart +++ b/lib/views/home/chat/widgets/message-list.dart @@ -1,12 +1,11 @@ -// Flutter imports: import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; +import 'package:syphon/global/print.dart'; -// Project imports: import 'package:syphon/global/themes.dart'; import 'package:syphon/store/events/actions.dart'; import 'package:syphon/store/events/messages/model.dart'; @@ -15,6 +14,7 @@ import 'package:syphon/store/events/selectors.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/rooms/room/model.dart'; import 'package:syphon/store/rooms/selectors.dart'; +import 'package:syphon/store/settings/chat-settings/selectors.dart'; import 'package:syphon/store/user/model.dart'; import 'package:syphon/store/user/selectors.dart'; import 'package:syphon/views/widgets/messages/message-typing.dart'; @@ -44,9 +44,7 @@ class MessageList extends StatefulWidget { } class MessageListState extends State { - MessageListState({ - Key? key, - }) : super(); + MessageListState() : super(); final TextEditingController controller = TextEditingController(); @protected @@ -100,8 +98,7 @@ class MessageListState extends State { @override Widget build(BuildContext context) => StoreConnector( distinct: true, - converter: (Store store) => - _Props.mapStateToProps(store, widget.roomId), + converter: (Store store) => _Props.mapStateToProps(store, widget.roomId), onInitialBuild: onMounted, builder: (context, props) { return GestureDetector( @@ -109,18 +106,14 @@ class MessageListState extends State { child: ListView( reverse: true, padding: EdgeInsets.only(bottom: 12), - physics: widget.selectedMessage != null - ? const NeverScrollableScrollPhysics() - : null, + physics: widget.selectedMessage != null ? const NeverScrollableScrollPhysics() : null, controller: widget.scrollController, children: [ MessageTypingWidget( roomUsers: props.users, typing: props.room.userTyping, usersTyping: props.room.usersTyping, - selectedMessageId: widget.selectedMessage != null - ? widget.selectedMessage!.id - : null, + selectedMessageId: widget.selectedMessage != null ? widget.selectedMessage!.id : null, onPressAvatar: widget.onViewUserDetails, ), ListView.builder( @@ -134,22 +127,15 @@ class MessageListState extends State { physics: const NeverScrollableScrollPhysics(), itemBuilder: (BuildContext context, int index) { final message = props.messages[index]; - final lastMessage = - index != 0 ? props.messages[index - 1] : null; - final nextMessage = index + 1 < props.messages.length - ? props.messages[index + 1] - : null; - - final isLastSender = lastMessage != null && - lastMessage.sender == message.sender; - final isNextSender = nextMessage != null && - nextMessage.sender == message.sender; - final isUserSent = - props.currentUser.userId == message.sender; - - final selectedMessageId = widget.selectedMessage != null - ? widget.selectedMessage!.id - : null; + final lastMessage = index != 0 ? props.messages[index - 1] : null; + final nextMessage = index + 1 < props.messages.length ? props.messages[index + 1] : null; + + final isLastSender = lastMessage != null && lastMessage.sender == message.sender; + final isNextSender = nextMessage != null && nextMessage.sender == message.sender; + final isUserSent = props.currentUser.userId == message.sender; + + final selectedMessageId = + widget.selectedMessage != null ? widget.selectedMessage!.id : null; final avatarUri = props.users[message.sender]?.avatarUri; @@ -162,12 +148,11 @@ class MessageListState extends State { selectedMessageId: selectedMessageId, avatarUri: avatarUri, theme: props.theme, - fontSize: 14, + color: props.chatColorPrimary, timeFormat: props.timeFormat24Enabled! ? '24hr' : '12hr', onSwipe: props.onSelectReply, onPressAvatar: widget.onViewUserDetails, - onLongPress: (msg) => - widget.onToggleSelectedMessage!(msg), + onLongPress: (msg) => widget.onToggleSelectedMessage!(msg), onInputReaction: () => onInputReaction( message: message, props: props, @@ -193,17 +178,19 @@ class _Props extends Equatable { final Map users; final List messages; final bool? timeFormat24Enabled; + final Color? chatColorPrimary; final Function onToggleReaction; final Function onSelectReply; - _Props({ + const _Props({ required this.room, required this.theme, required this.users, required this.messages, required this.currentUser, required this.timeFormat24Enabled, + required this.chatColorPrimary, required this.onToggleReaction, required this.onSelectReply, }); @@ -215,11 +202,11 @@ class _Props extends Equatable { messages, ]; - static _Props mapStateToProps(Store store, String? roomId) => - _Props( + static _Props mapStateToProps(Store store, String? roomId) => _Props( timeFormat24Enabled: store.state.settingsStore.timeFormat24Enabled, theme: store.state.settingsStore.theme, currentUser: store.state.authStore.user, + chatColorPrimary: selectBubbleColor(store, roomId), room: selectRoom(id: roomId, state: store.state), users: messageUsers(roomId: roomId, state: store.state), messages: latestMessages( @@ -231,8 +218,12 @@ class _Props extends Equatable { store.state, ), ), - onSelectReply: (Message message) { - store.dispatch(selectReply(roomId: roomId, message: message)); + onSelectReply: (Message? message) { + try { + store.dispatch(selectReply(roomId: roomId, message: message)); + } catch (error) { + printError(error.toString()); + } }, onToggleReaction: ({Message? message, String? emoji}) { final room = selectRoom(id: roomId, state: store.state); diff --git a/lib/views/home/groups/group-create-public-screen.dart b/lib/views/home/groups/group-create-public-screen.dart index ae8d122e3..5269b2b12 100644 --- a/lib/views/home/groups/group-create-public-screen.dart +++ b/lib/views/home/groups/group-create-public-screen.dart @@ -1,15 +1,13 @@ -// Dart imports: import 'dart:io'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; import 'package:syphon/global/colours.dart'; +import 'package:syphon/global/formatters.dart'; import 'package:syphon/global/themes.dart'; import 'package:syphon/store/rooms/actions.dart'; import 'package:syphon/store/rooms/room/model.dart'; @@ -19,7 +17,6 @@ import 'package:syphon/views/widgets/input/text-field-secure.dart'; import 'package:syphon/views/widgets/lists/list-user-bubbles.dart'; import 'package:syphon/views/widgets/modals/modal-image-options.dart'; -// Project imports: import 'package:syphon/views/behaviors.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; @@ -36,7 +33,7 @@ class CreatePublicGroupScreen extends StatefulWidget { } class CreateGroupPublicState extends State { - CreateGroupPublicState({Key? key}) : super(); + CreateGroupPublicState() : super(); File? avatar; String? name; @@ -52,16 +49,9 @@ class CreateGroupPublicState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - onMounted(); } - @protected - void onMounted() { - /** noop */ - } - - @protected - void onCreateRoom(_Props props) async { + onCreateRoom(_Props props) async { final roomId = await props.onCreateRoomPublic( avatar: avatar, name: name, @@ -73,14 +63,12 @@ class CreateGroupPublicState extends State { } } - @protected - void onQuit(_Props props) async { + onQuit(_Props props) async { props.onClearUserInvites(); Navigator.pop(context); } - @protected - void onShowImageOptions() async { + onShowImageOptions() async { await showModalBottomSheet( context: context, isScrollControlled: true, @@ -120,7 +108,7 @@ class CreateGroupPublicState extends State { avatarWidget = CircleAvatar( backgroundColor: Colours.hashedColor(name), child: Text( - formatInitials(name), + formatInitialsLong(name), style: TextStyle( color: Colors.white, fontSize: Dimensions.avatarFontSize(size: imageSize), @@ -169,7 +157,7 @@ class CreateGroupPublicState extends State { child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - FocusScope.of(context).requestFocus(new FocusNode()); + FocusScope.of(context).requestFocus(FocusNode()); }, child: ConstrainedBox( constraints: BoxConstraints( @@ -190,21 +178,17 @@ class CreateGroupPublicState extends State { Flexible( flex: 0, child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Stack( children: [ Container( - margin: EdgeInsets.only( - top: 42, bottom: 8), + margin: EdgeInsets.only(top: 42, bottom: 8), width: imageSize, height: imageSize, child: GestureDetector( - onTap: () => - onShowImageOptions(), + onTap: () => onShowImageOptions(), child: avatarWidget, ), ), @@ -212,14 +196,11 @@ class CreateGroupPublicState extends State { right: 6, bottom: 2, child: Container( - width: - Dimensions.iconSizeLarge, - height: - Dimensions.iconSizeLarge, + width: Dimensions.iconSizeLarge, + height: Dimensions.iconSizeLarge, decoration: BoxDecoration( color: backgroundColor, - borderRadius: - BorderRadius.circular( + borderRadius: BorderRadius.circular( Dimensions.iconSizeLarge, ), boxShadow: [ @@ -231,11 +212,8 @@ class CreateGroupPublicState extends State { ), child: Icon( Icons.camera_alt, - color: Theme.of(context) - .iconTheme - .color, - size: - Dimensions.iconSizeLite, + color: Theme.of(context).iconTheme.color, + size: Dimensions.iconSizeLite, ), ), ), @@ -244,60 +222,44 @@ class CreateGroupPublicState extends State { Container( padding: EdgeInsets.only(top: 12), child: Column( - crossAxisAlignment: - CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( - padding: EdgeInsets.only( - bottom: 4), + padding: EdgeInsets.only(bottom: 4), child: Text( name ?? '', - overflow: - TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText1, ), ), Visibility( - visible: alias != null && - alias!.isNotEmpty, + visible: alias != null && alias!.isNotEmpty, maintainSize: true, maintainState: true, maintainAnimation: true, child: Text( formatAlias( resource: alias ?? '', - homeserver: - props.homeserver ?? - '', + homeserver: props.homeserver ?? '', ), textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .caption, + style: Theme.of(context).textTheme.caption, ), ), Flexible( flex: 0, fit: FlexFit.tight, child: Container( - padding: EdgeInsets.only( - top: 4), - constraints: - BoxConstraints( + padding: EdgeInsets.only(top: 4), + constraints: BoxConstraints( maxWidth: width / 1.5, ), child: Text( topic ?? '', maxLines: 1, - overflow: TextOverflow - .ellipsis, - textAlign: - TextAlign.center, - style: Theme.of(context) - .textTheme - .caption, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.caption, ), )), ], @@ -310,44 +272,33 @@ class CreateGroupPublicState extends State { flex: 0, fit: FlexFit.loose, child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: Dimensions.listPadding, child: Text( 'About', textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .subtitle2, + style: Theme.of(context).textTheme.subtitle2, ), ), Container( margin: Dimensions.inputMargin, constraints: BoxConstraints( - maxHeight: - Dimensions.inputHeight, - maxWidth: - Dimensions.inputWidthMax, + maxHeight: Dimensions.inputHeight, + maxWidth: Dimensions.inputWidthMax, ), child: TextFieldSecure( label: 'Name*', - textInputAction: - TextInputAction.next, + textInputAction: TextInputAction.next, controller: nameController, onSubmitted: (text) => - FocusScope.of(context) - .requestFocus( - aliasFocus), - onChanged: (text) => - setState(() { + FocusScope.of(context).requestFocus(aliasFocus), + onChanged: (text) => setState(() { name = text; }), ), @@ -355,23 +306,17 @@ class CreateGroupPublicState extends State { Container( margin: Dimensions.inputMargin, constraints: BoxConstraints( - maxHeight: - Dimensions.inputHeight, - maxWidth: - Dimensions.inputWidthMax, + maxHeight: Dimensions.inputHeight, + maxWidth: Dimensions.inputWidthMax, ), child: TextFieldSecure( label: 'Alias*', - textInputAction: - TextInputAction.next, + textInputAction: TextInputAction.next, disableSpacing: true, focusNode: aliasFocus, onSubmitted: (text) => - FocusScope.of(context) - .requestFocus( - topicFocus), - onChanged: (text) => - setState(() { + FocusScope.of(context).requestFocus(topicFocus), + onChanged: (text) => setState(() { alias = text; }), controller: aliasController, @@ -379,23 +324,18 @@ class CreateGroupPublicState extends State { ), Container( margin: Dimensions.inputMargin, - height: Dimensions - .inputEditorHeight, + height: Dimensions.inputEditorHeight, constraints: BoxConstraints( - maxHeight: Dimensions - .inputEditorHeight, - maxWidth: - Dimensions.inputWidthMax, + maxHeight: Dimensions.inputEditorHeight, + maxWidth: Dimensions.inputWidthMax, ), child: TextFieldSecure( label: 'Topic', maxLines: 25, focusNode: topicFocus, controller: topicController, - textInputAction: - TextInputAction.newline, - onChanged: (text) => - setState(() { + textInputAction: TextInputAction.newline, + onChanged: (text) => setState(() { topic = text; }), ), @@ -409,10 +349,8 @@ class CreateGroupPublicState extends State { flex: 0, fit: FlexFit.loose, child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: EdgeInsets.only( @@ -425,9 +363,7 @@ class CreateGroupPublicState extends State { Text( Strings.labelUsersSection, textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .subtitle2, + style: Theme.of(context).textTheme.subtitle2, ), ], ), @@ -450,10 +386,8 @@ class CreateGroupPublicState extends State { child: Container( padding: EdgeInsets.only(top: 16), child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( margin: const EdgeInsets.all(8.0), @@ -461,19 +395,15 @@ class CreateGroupPublicState extends State { text: Strings.buttonCreate, loading: props.loading, disabled: props.loading, - onPressed: () => - onCreateRoom(props), + onPressed: () => onCreateRoom(props), ), ), Container( height: Dimensions.inputHeight, - margin: - const EdgeInsets.all(10.0), + margin: const EdgeInsets.all(10.0), constraints: BoxConstraints( - minWidth: - Dimensions.buttonWidthMin, - minHeight: - Dimensions.buttonHeightMin, + minWidth: Dimensions.buttonWidthMin, + minHeight: Dimensions.buttonHeightMin, ), child: ButtonTextOpacity( text: Strings.buttonQuit, diff --git a/lib/views/home/groups/group-create-screen.dart b/lib/views/home/groups/group-create-screen.dart index 3defee148..0223a40a6 100644 --- a/lib/views/home/groups/group-create-screen.dart +++ b/lib/views/home/groups/group-create-screen.dart @@ -1,15 +1,13 @@ -// Dart imports: import 'dart:io'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; import 'package:syphon/global/colours.dart'; +import 'package:syphon/global/formatters.dart'; import 'package:syphon/global/themes.dart'; import 'package:syphon/store/alerts/actions.dart'; import 'package:syphon/store/rooms/actions.dart'; @@ -21,7 +19,6 @@ import 'package:syphon/views/widgets/input/text-field-secure.dart'; import 'package:syphon/views/widgets/lists/list-user-bubbles.dart'; import 'package:syphon/views/widgets/modals/modal-image-options.dart'; -// Project imports: import 'package:syphon/views/behaviors.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; @@ -38,7 +35,7 @@ class CreateGroupScreen extends StatefulWidget { } class CreateGroupPublicState extends State { - CreateGroupPublicState({Key? key}) : super(); + CreateGroupPublicState() : super(); final topicFocus = FocusNode(); final nameController = TextEditingController(); final topicController = TextEditingController(); @@ -53,27 +50,24 @@ class CreateGroupPublicState extends State { super.didChangeDependencies(); } - @protected - void onCreateRoom(_Props props) async { + onCreateRoom(_Props props) async { final roomId = await props.onCreateGroup( - name: this.name, - topic: this.topic, - avatar: this.avatar, - encryption: this.encryption, + name: name, + topic: topic, + avatar: avatar, + encryption: encryption, ); if (roomId != null) { Navigator.pop(context); } } - @protected - void onQuit(_Props props) async { + onQuit(_Props props) async { props.onClearUserInvites(); Navigator.pop(context); } - @protected - void onToggleEncryption(_Props props) async { + onToggleEncryption(_Props props) async { return showDialog( context: context, barrierDismissible: false, @@ -81,15 +75,14 @@ class CreateGroupPublicState extends State { content: Strings.confirmationGroupEncryption, onAccept: () { setState(() { - encryption = !this.encryption; + encryption = !encryption; }); }, ), ); } - @protected - void onShowImageOptions() async { + onShowImageOptions() async { await showModalBottomSheet( context: context, backgroundColor: Colors.transparent, @@ -124,11 +117,11 @@ class CreateGroupPublicState extends State { ), ); - if (this.name != null && this.name!.isNotEmpty) { + if (name != null && name!.isNotEmpty) { avatarWidget = CircleAvatar( - backgroundColor: Colours.hashedColor(this.name), + backgroundColor: Colours.hashedColor(name), child: Text( - formatInitials(this.name), + formatInitialsLong(name), style: TextStyle( color: Colors.white, fontSize: Dimensions.avatarFontSize(size: imageSize), @@ -137,11 +130,11 @@ class CreateGroupPublicState extends State { ); } - if (this.avatar != null) { + if (avatar != null) { avatarWidget = ClipRRect( borderRadius: BorderRadius.circular(imageSize), child: Image.file( - this.avatar!, + avatar!, width: imageSize, height: imageSize, fit: BoxFit.cover, @@ -177,7 +170,7 @@ class CreateGroupPublicState extends State { child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - FocusScope.of(context).requestFocus(new FocusNode()); + FocusScope.of(context).requestFocus(FocusNode()); }, child: ConstrainedBox( constraints: BoxConstraints( @@ -198,21 +191,17 @@ class CreateGroupPublicState extends State { Flexible( flex: 0, child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Stack( children: [ Container( - margin: EdgeInsets.only( - top: 42, bottom: 8), + margin: EdgeInsets.only(top: 42, bottom: 8), width: imageSize, height: imageSize, child: GestureDetector( - onTap: () => - onShowImageOptions(), + onTap: () => onShowImageOptions(), child: avatarWidget, ), ), @@ -220,14 +209,11 @@ class CreateGroupPublicState extends State { right: 6, bottom: 2, child: Container( - width: - Dimensions.iconSizeLarge, - height: - Dimensions.iconSizeLarge, + width: Dimensions.iconSizeLarge, + height: Dimensions.iconSizeLarge, decoration: BoxDecoration( color: backgroundColor, - borderRadius: - BorderRadius.circular( + borderRadius: BorderRadius.circular( Dimensions.iconSizeLarge, ), boxShadow: [ @@ -240,11 +226,8 @@ class CreateGroupPublicState extends State { ), child: Icon( Icons.camera_alt, - color: Theme.of(context) - .iconTheme - .color, - size: - Dimensions.iconSizeLite, + color: Theme.of(context).iconTheme.color, + size: Dimensions.iconSizeLite, ), ), ), @@ -253,41 +236,30 @@ class CreateGroupPublicState extends State { Container( padding: EdgeInsets.only(top: 12), child: Column( - crossAxisAlignment: - CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( - padding: EdgeInsets.only( - bottom: 4), + padding: EdgeInsets.only(bottom: 4), child: Text( - this.name ?? '', - overflow: - TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1, + name ?? '', + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText1, ), ), Flexible( flex: 0, fit: FlexFit.tight, child: Container( - padding: EdgeInsets.only( - top: 4), - constraints: - BoxConstraints( + padding: EdgeInsets.only(top: 4), + constraints: BoxConstraints( maxWidth: width / 1.5, ), child: Text( - this.topic ?? '', + topic ?? '', maxLines: 1, - overflow: TextOverflow - .ellipsis, - textAlign: - TextAlign.center, - style: Theme.of(context) - .textTheme - .caption, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.caption, ), )), ], @@ -300,67 +272,51 @@ class CreateGroupPublicState extends State { flex: 0, fit: FlexFit.loose, child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: Dimensions.listPadding, child: Text( 'About', textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .subtitle2, + style: Theme.of(context).textTheme.subtitle2, ), ), Container( margin: Dimensions.inputMargin, constraints: BoxConstraints( - maxHeight: - Dimensions.inputHeight, - maxWidth: - Dimensions.inputWidthMax, + maxHeight: Dimensions.inputHeight, + maxWidth: Dimensions.inputWidthMax, ), child: TextFieldSecure( label: 'Name*', - textInputAction: - TextInputAction.next, + textInputAction: TextInputAction.next, controller: nameController, onSubmitted: (text) => - FocusScope.of(context) - .requestFocus( - topicFocus), - onChanged: (text) => - setState(() { + FocusScope.of(context).requestFocus(topicFocus), + onChanged: (text) => setState(() { name = text; }), ), ), Container( margin: Dimensions.inputMargin, - height: Dimensions - .inputEditorHeight, + height: Dimensions.inputEditorHeight, constraints: BoxConstraints( - maxHeight: Dimensions - .inputEditorHeight, - maxWidth: - Dimensions.inputWidthMax, + maxHeight: Dimensions.inputEditorHeight, + maxWidth: Dimensions.inputWidthMax, ), child: TextFieldSecure( label: 'Topic', maxLines: 25, focusNode: topicFocus, controller: topicController, - textInputAction: - TextInputAction.newline, - onChanged: (text) => - setState(() { + textInputAction: TextInputAction.newline, + onChanged: (text) => setState(() { topic = text; }), ), @@ -374,10 +330,8 @@ class CreateGroupPublicState extends State { flex: 0, fit: FlexFit.loose, child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: EdgeInsets.only( @@ -390,9 +344,7 @@ class CreateGroupPublicState extends State { Text( Strings.labelUsersSection, textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .subtitle2, + style: Theme.of(context).textTheme.subtitle2, ), ], ), @@ -414,10 +366,8 @@ class CreateGroupPublicState extends State { flex: 0, fit: FlexFit.loose, child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: EdgeInsets.only( @@ -430,9 +380,7 @@ class CreateGroupPublicState extends State { Text( 'Options', textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .subtitle2, + style: Theme.of(context).textTheme.subtitle2, ), ], ), @@ -440,23 +388,16 @@ class CreateGroupPublicState extends State { Container( width: width / 1.3, child: ListTile( - contentPadding: - Dimensions.listPadding, + contentPadding: Dimensions.listPadding, title: Text( 'Message Encryption', - style: Theme.of(context) - .textTheme - .subtitle1, + style: Theme.of(context).textTheme.subtitle1, ), - trailing: Container( - child: Switch( - value: this.encryption, - onChanged: (value) => - onToggleEncryption(props), - ), + trailing: Switch( + value: encryption, + onChanged: (value) => onToggleEncryption(props), ), - onTap: () => - onToggleEncryption(props), + onTap: () => onToggleEncryption(props), ), ), ], @@ -467,10 +408,8 @@ class CreateGroupPublicState extends State { child: Container( padding: EdgeInsets.only(top: 16), child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( margin: const EdgeInsets.all(8.0), @@ -478,24 +417,19 @@ class CreateGroupPublicState extends State { text: Strings.buttonCreate, loading: props.loading, disabled: props.loading, - onPressed: () => - this.onCreateRoom(props), + onPressed: () => onCreateRoom(props), ), ), Container( height: Dimensions.inputHeight, - margin: - const EdgeInsets.all(10.0), + margin: const EdgeInsets.all(10.0), constraints: BoxConstraints( - minWidth: - Dimensions.buttonWidthMin, - minHeight: - Dimensions.buttonHeightMin, + minWidth: Dimensions.buttonWidthMin, + minHeight: Dimensions.buttonHeightMin, ), child: ButtonTextOpacity( text: Strings.buttonQuit, - onPressed: () => - this.onQuit(props), + onPressed: () => onQuit(props), ), ), ], diff --git a/lib/views/home/groups/invite-users-screen.dart b/lib/views/home/groups/invite-users-screen.dart index 3cd1676e9..807630c77 100644 --- a/lib/views/home/groups/invite-users-screen.dart +++ b/lib/views/home/groups/invite-users-screen.dart @@ -1,11 +1,8 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/svg.dart'; @@ -18,7 +15,6 @@ import 'package:syphon/store/user/actions.dart'; import 'package:syphon/views/widgets/appbars/appbar-search.dart'; import 'package:syphon/views/widgets/containers/card-section.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/formatters.dart'; diff --git a/lib/views/home/home-screen.dart b/lib/views/home/home-screen.dart index 408699cde..2eeadfcc6 100644 --- a/lib/views/home/home-screen.dart +++ b/lib/views/home/home-screen.dart @@ -1,11 +1,7 @@ -// Flutter imports: -import 'dart:convert'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:fab_circular_menu/fab_circular_menu.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -16,7 +12,6 @@ import 'package:syphon/global/themes.dart'; import 'package:syphon/store/events/selectors.dart'; import 'package:url_launcher/url_launcher.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/formatters.dart'; @@ -32,7 +27,7 @@ import 'package:syphon/store/rooms/selectors.dart'; import 'package:syphon/store/settings/chat-settings/model.dart'; import 'package:syphon/store/sync/actions.dart'; import 'package:syphon/store/user/model.dart'; -import 'package:syphon/views/home/chat/details-chat-screen.dart'; +import 'package:syphon/views/home/chat/chat-detail-screen.dart'; import 'package:syphon/views/home/chat/chat-screen.dart'; import 'package:syphon/views/widgets/avatars/avatar-app-bar.dart'; import 'package:syphon/views/widgets/avatars/avatar.dart'; @@ -49,19 +44,12 @@ class HomeScreen extends StatefulWidget { } class HomeState extends State { - HomeState({Key? key}) : super(); + HomeState() : super(); - final GlobalKey fabKey = - GlobalKey(); + final fabKey = GlobalKey(); Room? selectedRoom; - late Map roomColorDefaults; - - @override - void initState() { - super.initState(); - roomColorDefaults = Map(); - } + Map roomColorDefaults = {}; @protected onToggleRoomOptions({Room? room}) { @@ -78,9 +66,8 @@ class HomeState extends State { } @protected - Widget buildAppBarRoomOptions({BuildContext? context, _Props? props}) => - AppBar( - backgroundColor: Colors.grey[500], + Widget buildAppBarRoomOptions({BuildContext? context, _Props? props}) => AppBar( + backgroundColor: Color(Colours.greyDefault), automaticallyImplyLeading: false, titleSpacing: 0.0, title: Row( @@ -107,7 +94,7 @@ class HomeState extends State { Navigator.pushNamed( context!, '/home/chat/settings', - arguments: ChatSettingsArguments( + arguments: ChatDetailArguments( roomId: selectedRoom!.id, title: selectedRoom!.name, ), @@ -120,7 +107,7 @@ class HomeState extends State { tooltip: 'Archive Room', color: Colors.white, onPressed: () async { - await props!.onArchiveRoom(room: this.selectedRoom); + await props!.onArchiveRoom(room: selectedRoom); setState(() { selectedRoom = null; }); @@ -134,7 +121,7 @@ class HomeState extends State { tooltip: 'Leave Chat', color: Colors.white, onPressed: () async { - await props!.onLeaveChat(room: this.selectedRoom); + await props!.onLeaveChat(room: selectedRoom); setState(() { selectedRoom = null; }); @@ -142,14 +129,14 @@ class HomeState extends State { ), ), Visibility( - visible: this.selectedRoom!.direct, + visible: selectedRoom!.direct, child: IconButton( icon: Icon(Icons.delete_outline), iconSize: Dimensions.buttonAppBarSize, tooltip: 'Delete Chat', color: Colors.white, onPressed: () async { - await props!.onDeleteChat(room: this.selectedRoom); + await props!.onDeleteChat(room: selectedRoom); setState(() { selectedRoom = null; }); @@ -167,8 +154,7 @@ class HomeState extends State { ); @protected - Widget buildAppBar({required BuildContext context, required _Props props}) => - AppBar( + Widget buildAppBar({required BuildContext context, required _Props props}) => AppBar( automaticallyImplyLeading: false, brightness: Brightness.dark, titleSpacing: 16.00, @@ -251,7 +237,9 @@ class HomeState extends State { ); @protected - Widget buildChatList(List rooms, BuildContext context, _Props props) { + Widget buildChatList(BuildContext context, _Props props) { + final rooms = props.rooms; + if (rooms.isEmpty) { return Center( child: Column( @@ -288,23 +276,25 @@ class HomeState extends State { itemBuilder: (BuildContext context, int index) { final room = rooms[index]; final messages = props.messages[room.id] ?? const []; - final messageLatest = latestMessage(messages); final chatSettings = props.chatSettings[room.id]; + + final messageLatest = latestMessage(messages); final preview = formatPreview(room: room, message: messageLatest); + final roomName = room.name ?? ''; final newMessage = messageLatest != null && - room.lastRead < messageLatest.timestamp! && + room.lastRead < messageLatest.timestamp && messageLatest.sender != props.currentUser.userId; var backgroundColor; var textStyle = TextStyle(); - var primaryColor = Colors.grey[500]; + Color primaryColor = Colors.grey; // Check settings for custom color, then check temp cache, // or generate new temp color if (chatSettings != null) { primaryColor = Color(chatSettings.primaryColor); } else if (roomColorDefaults.containsKey(room.id)) { - primaryColor = roomColorDefaults[room.id]; + primaryColor = roomColorDefaults[room.id] ?? primaryColor; } else { primaryColor = Colours.hashedColor(room.id); roomColorDefaults.putIfAbsent( @@ -329,8 +319,7 @@ class HomeState extends State { if (messages.isNotEmpty && messageLatest != null) { // it has undecrypted message contained within - if (messageLatest.type == EventTypes.encrypted && - messageLatest.body!.isEmpty) { + if (messageLatest.type == EventTypes.encrypted && messageLatest.body!.isEmpty) { textStyle = TextStyle(fontStyle: FontStyle.italic); } @@ -358,7 +347,7 @@ class HomeState extends State { '/home/chat', arguments: ChatViewArguements( roomId: room.id, - title: room.name, + title: roomName, ), ); } @@ -444,9 +433,7 @@ class HomeState extends State { ), ), Visibility( - visible: props.roomTypeBadgesEnabled && - room.type == 'group' && - !room.invite, + visible: props.roomTypeBadgesEnabled && room.type == 'group' && !room.invite, child: Positioned( right: 0, bottom: 0, @@ -466,9 +453,7 @@ class HomeState extends State { ), ), Visibility( - visible: props.roomTypeBadgesEnabled && - room.type == 'public' && - !room.invite, + visible: props.roomTypeBadgesEnabled && room.type == 'public' && !room.invite, child: Positioned( right: 0, bottom: 0, @@ -504,15 +489,14 @@ class HomeState extends State { children: [ Expanded( child: Text( - room.name!, + roomName, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyText1, ), ), Text( formatTimestamp(lastUpdateMillis: room.lastUpdate), - style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w100), + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w100), ), ], ), @@ -570,7 +554,6 @@ class HomeState extends State { GestureDetector( onTap: onDismissMessageOptions, child: buildChatList( - props.rooms, context, props, ), @@ -628,12 +611,14 @@ class _Props extends Equatable { @override List get props => [ rooms, + messages, theme, syncing, offline, unauthed, currentUser, chatSettings, + roomTypeBadgesEnabled, ]; static _Props mapStateToProps(Store store) => _Props( @@ -652,13 +637,10 @@ class _Props extends Equatable { final backgrounded = store.state.syncStore.backgrounded; final loadingRooms = store.state.roomStore.loading; - final lastAttempt = DateTime.fromMillisecondsSinceEpoch( - store.state.syncStore.lastAttempt ?? 0); + final lastAttempt = DateTime.fromMillisecondsSinceEpoch(store.state.syncStore.lastAttempt ?? 0); // See if the last attempted sy nc is older than 60 seconds - final isLastAttemptOld = DateTime.now() - .difference(lastAttempt) - .compareTo(Duration(seconds: 90)); + final isLastAttemptOld = DateTime.now().difference(lastAttempt).compareTo(Duration(seconds: 90)); // syncing for the first time if (syncing && !synced) { diff --git a/lib/views/home/profile/profile-screen.dart b/lib/views/home/profile/profile-screen.dart index c82364f9c..b4b7e3527 100644 --- a/lib/views/home/profile/profile-screen.dart +++ b/lib/views/home/profile/profile-screen.dart @@ -1,11 +1,8 @@ -// Dart imports: import 'dart:io'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; @@ -15,7 +12,6 @@ import 'package:syphon/views/widgets/avatars/avatar.dart'; import 'package:syphon/views/widgets/input/text-field-secure.dart'; import 'package:touchable_opacity/touchable_opacity.dart'; -// Project imports: import 'package:syphon/views/behaviors.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/home/profile/profile-user-screen.dart b/lib/views/home/profile/profile-user-screen.dart index 537b18ac1..0d6b8df7f 100644 --- a/lib/views/home/profile/profile-user-screen.dart +++ b/lib/views/home/profile/profile-user-screen.dart @@ -1,8 +1,6 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; @@ -10,7 +8,6 @@ import 'package:syphon/global/colours.dart'; import 'package:syphon/store/user/actions.dart'; import 'package:syphon/views/widgets/containers/card-section.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/user/model.dart'; @@ -116,15 +113,13 @@ class UserProfileState extends State { final titlePadding = Dimensions.listTitlePaddingDynamic(width: width); final contentPadding = Dimensions.listPaddingDynamic(width: width); - final UserDetailsArguments arguments = - ModalRoute.of(context)!.settings.arguments as UserDetailsArguments; + final UserDetailsArguments arguments = ModalRoute.of(context)!.settings.arguments as UserDetailsArguments; final user = arguments.user!; final userColor = Colours.hashedColor(user.userId); - final scaffordBackgroundColor = - Theme.of(context).brightness == Brightness.light - ? Colors.grey[200] - : Theme.of(context).scaffoldBackgroundColor; + final scaffordBackgroundColor = Theme.of(context).brightness == Brightness.light + ? Color(Colours.greyLightest) + : Theme.of(context).scaffoldBackgroundColor; return StoreConnector( distinct: true, diff --git a/lib/views/home/search/search-groups-screen.dart b/lib/views/home/search/search-groups-screen.dart index 8775a0172..b0e0c186d 100644 --- a/lib/views/home/search/search-groups-screen.dart +++ b/lib/views/home/search/search-groups-screen.dart @@ -1,8 +1,6 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:expandable/expandable.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -14,7 +12,6 @@ import 'package:syphon/global/colours.dart'; import 'package:syphon/global/values.dart'; import 'package:syphon/views/widgets/appbars/appbar-search.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/global/themes.dart'; diff --git a/lib/views/home/search/search-rooms-screen.dart b/lib/views/home/search/search-rooms-screen.dart index 7e7994f47..66d08d802 100644 --- a/lib/views/home/search/search-rooms-screen.dart +++ b/lib/views/home/search/search-rooms-screen.dart @@ -1,8 +1,6 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/svg.dart'; @@ -14,7 +12,6 @@ import 'package:syphon/store/user/model.dart'; import 'package:syphon/store/user/selectors.dart'; import 'package:syphon/views/widgets/appbars/appbar-search.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/global/themes.dart'; @@ -42,14 +39,14 @@ class RoomSearchScreen extends StatefulWidget { class RoomSearchState extends State { final searchInputFocusNode = FocusNode(); - RoomSearchState({Key? key}); + RoomSearchState(); - late Map roomColorDefaults; + late Map roomColorDefaults; @override void initState() { super.initState(); - roomColorDefaults = Map(); + roomColorDefaults = {}; } // componentDidMount(){} @@ -79,8 +76,7 @@ class RoomSearchState extends State { Future onInviteUser(_Props props, Room room) async { FocusScope.of(context).unfocus(); - final RoomSearchArguments arguments = - ModalRoute.of(context)!.settings.arguments as RoomSearchArguments; + final RoomSearchArguments arguments = ModalRoute.of(context)!.settings.arguments as RoomSearchArguments; final user = arguments.user!; final username = formatUsername(user); @@ -89,8 +85,7 @@ class RoomSearchState extends State { builder: (BuildContext context) => DialogStartChat( user: user, title: 'Invite $username', - content: Strings.confirmationInvite + - '\n\nUser: $username\nRoom: ${room.name}', + content: '${Strings.confirmationInvite}\n\nUser: $username\nRoom: ${room.name}', action: 'send invite', onStartChat: () async { props.onSendInvite(room: room, user: user); @@ -148,28 +143,24 @@ class RoomSearchState extends State { final room = rooms[index]; final chatSettings = props.chatSettings[room.id]; - bool messagesNew = false; var previewStyle; var preview = room.topic; - var backgroundColor = Colors.grey[500]; + var backgroundColor = Color(Colours.greyDefault); if (preview == null || preview.isEmpty) { preview = 'No Description'; previewStyle = TextStyle(fontStyle: FontStyle.italic); } - // Check settings for custom color, then check temp cache, - // or generate new temp color + // Check settings for custom color, + // then check temp cache, or generate new temp color if (chatSettings != null) { backgroundColor = Color(chatSettings.primaryColor); } else if (roomColorDefaults.containsKey(room.id)) { - backgroundColor = roomColorDefaults[room.id]; + backgroundColor = roomColorDefaults[room.id]!; } else { backgroundColor = Colours.hashedColor(room.id); - roomColorDefaults.putIfAbsent( - room.id, - () => backgroundColor, - ); + roomColorDefaults.putIfAbsent(room.id, () => backgroundColor); } // GestureDetector w/ animation @@ -233,21 +224,6 @@ class RoomSearchState extends State { ), ), ), - Visibility( - visible: messagesNew, - child: Positioned( - top: 0, - right: 0, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Container( - width: Dimensions.badgeAvatarSizeSmall, - height: Dimensions.badgeAvatarSizeSmall, - color: Theme.of(context).accentColor, - ), - ), - ), - ), Visibility( visible: room.type == 'group' && !room.invite, child: Positioned( diff --git a/lib/views/home/search/search-users-screen.dart b/lib/views/home/search/search-users-screen.dart index 83904a0e5..7471af131 100644 --- a/lib/views/home/search/search-users-screen.dart +++ b/lib/views/home/search/search-users-screen.dart @@ -1,11 +1,8 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/svg.dart'; @@ -16,7 +13,6 @@ import 'package:syphon/views/widgets/containers/card-section.dart'; import 'package:syphon/views/widgets/loader/index.dart'; import 'package:syphon/views/widgets/modals/modal-user-details.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/formatters.dart'; diff --git a/lib/views/home/settings/advanced-settings-screen.dart b/lib/views/home/settings/advanced-settings-screen.dart index 3608f4220..b1d59da41 100644 --- a/lib/views/home/settings/advanced-settings-screen.dart +++ b/lib/views/home/settings/advanced-settings-screen.dart @@ -1,8 +1,6 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter/services.dart'; @@ -11,7 +9,6 @@ import 'package:intl/intl.dart'; import 'package:package_info/package_info.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/notifications.dart'; diff --git a/lib/views/home/settings/blocked-screen.dart b/lib/views/home/settings/blocked-screen.dart index 6ddf8100a..5f2f9b01c 100644 --- a/lib/views/home/settings/blocked-screen.dart +++ b/lib/views/home/settings/blocked-screen.dart @@ -1,11 +1,8 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; @@ -13,7 +10,6 @@ import 'package:syphon/views/widgets/containers/card-section.dart'; import 'package:syphon/views/widgets/loader/index.dart'; import 'package:syphon/views/widgets/modals/modal-user-details.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/index.dart'; diff --git a/lib/views/home/settings/password/password-update-screen.dart b/lib/views/home/settings/password/password-update-screen.dart index a093210ab..36af3ebfb 100644 --- a/lib/views/home/settings/password/password-update-screen.dart +++ b/lib/views/home/settings/password/password-update-screen.dart @@ -1,17 +1,10 @@ -// Dart imports: -import 'dart:async'; - -// Flutter imports: -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/views/behaviors.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; @@ -19,7 +12,7 @@ import 'package:syphon/global/values.dart'; import 'package:syphon/store/auth/actions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/views/widgets/buttons/button-solid.dart'; -import 'step-password.dart'; +import 'password-update-step.dart'; final Duration nextAnimationDuration = Duration( milliseconds: Values.animationDurationDefault, @@ -39,7 +32,7 @@ class PasswordUpdateState extends State { PageController? pageController; var sections = [ - PasswordStep(), + PasswordUpdateStep(), ]; PasswordUpdateState({Key? key}); diff --git a/lib/views/home/settings/password/step-password.dart b/lib/views/home/settings/password/password-update-step.dart similarity index 96% rename from lib/views/home/settings/password/step-password.dart rename to lib/views/home/settings/password/password-update-step.dart index dbb7f8736..fc743535f 100644 --- a/lib/views/home/settings/password/step-password.dart +++ b/lib/views/home/settings/password/password-update-step.dart @@ -1,32 +1,26 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; +import 'package:syphon/global/strings.dart'; import 'package:syphon/store/auth/actions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/views/widgets/input/text-field-secure.dart'; -// Store +class PasswordUpdateStep extends StatefulWidget { + const PasswordUpdateStep({Key? key}) : super(key: key); -// Styling - -class PasswordStep extends StatefulWidget { - const PasswordStep({Key? key}) : super(key: key); - - PasswordStepState createState() => PasswordStepState(); + PasswordUpdateStepState createState() => PasswordUpdateStepState(); } -class PasswordStepState extends State { - PasswordStepState({Key? key}); +class PasswordUpdateStepState extends State { + PasswordUpdateStepState({Key? key}); bool visibility = false; FocusNode currentFocusNode = FocusNode(); @@ -79,7 +73,7 @@ class PasswordStepState extends State { Container( padding: EdgeInsets.only(bottom: 8, top: 8), child: Text( - 'Come up with 4 random words\nyou\'ll easily remember', + Strings.passwordRecommendationDefault, textAlign: TextAlign.center, style: Theme.of(context).textTheme.caption, ), @@ -242,7 +236,7 @@ class _Props extends Equatable { final Function onChangePasswordConfirm; final Function onChangeCurrentPassword; - _Props({ + const _Props({ required this.password, required this.passwordCurrent, required this.passwordConfirm, diff --git a/lib/views/home/settings/settings-chat-screen.dart b/lib/views/home/settings/settings-chats-screen.dart similarity index 94% rename from lib/views/home/settings/settings-chat-screen.dart rename to lib/views/home/settings/settings-chats-screen.dart index 1b2f5cabc..2de8e2005 100644 --- a/lib/views/home/settings/settings-chat-screen.dart +++ b/lib/views/home/settings/settings-chats-screen.dart @@ -1,14 +1,11 @@ -// Flutter imports: import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/string-keys.dart'; @@ -17,8 +14,8 @@ import 'package:syphon/store/index.dart'; import 'package:syphon/store/settings/actions.dart'; import 'package:syphon/views/widgets/containers/card-section.dart'; -class ChatSettingsScreen extends StatelessWidget { - const ChatSettingsScreen({Key? key}) : super(key: key); +class ChatsSettingsScreen extends StatelessWidget { + const ChatsSettingsScreen({Key? key}) : super(key: key); displayThemeType(String themeTypeName) { return themeTypeName.split('.')[1].toLowerCase(); @@ -27,10 +24,9 @@ class ChatSettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) => StoreConnector( distinct: true, - converter: (Store store) => - Props.mapStateToProps(store, context), + converter: (Store store) => Props.mapStateToProps(store, context), builder: (context, props) { - double width = MediaQuery.of(context).size.width; + final double width = MediaQuery.of(context).size.width; return Scaffold( appBar: AppBar( @@ -88,8 +84,7 @@ class ChatSettingsScreen extends StatelessWidget { ), trailing: Switch( value: false, - inactiveThumbColor: - Color(Colours.greyDisabled), + inactiveThumbColor: Color(Colours.greyDisabled), onChanged: (showMembershipEvents) {}, ), ), @@ -106,8 +101,7 @@ class ChatSettingsScreen extends StatelessWidget { ), trailing: Switch( value: props.enterSend!, - onChanged: (enterSend) => - props.onToggleEnterSend(), + onChanged: (enterSend) => props.onToggleEnterSend(), ), ), ListTile( @@ -122,8 +116,7 @@ class ChatSettingsScreen extends StatelessWidget { ), trailing: Switch( value: props.timeFormat24!, - onChanged: (value) => - props.onToggleTimeFormat(), + onChanged: (value) => props.onToggleTimeFormat(), ), ), ListTile( @@ -138,8 +131,7 @@ class ChatSettingsScreen extends StatelessWidget { ), trailing: Switch( value: props.dismissKeyboard!, - onChanged: (value) => - props.onToggleDismissKeyboard(), + onChanged: (value) => props.onToggleDismissKeyboard(), ), ), ], @@ -293,7 +285,7 @@ class Props extends Equatable { final Function onToggleTimeFormat; final Function onToggleDismissKeyboard; - Props({ + const Props({ required this.language, required this.enterSend, required this.chatFontSize, @@ -314,8 +306,7 @@ class Props extends Equatable { timeFormat24, ]; - static Props mapStateToProps(Store store, BuildContext context) => - Props( + static Props mapStateToProps(Store store, BuildContext context) => Props( chatFontSize: 'Default', language: store.state.settingsStore.language, enterSend: store.state.settingsStore.enterSendEnabled, diff --git a/lib/views/home/settings/settings-devices-screen.dart b/lib/views/home/settings/settings-devices-screen.dart index 1676acef4..b0553f344 100644 --- a/lib/views/home/settings/settings-devices-screen.dart +++ b/lib/views/home/settings/settings-devices-screen.dart @@ -1,17 +1,15 @@ -// Flutter imports: import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; +import 'package:syphon/global/strings.dart'; import 'package:syphon/store/auth/actions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/settings/actions.dart'; @@ -25,7 +23,7 @@ class DevicesScreen extends StatefulWidget { } class DeviceViewState extends State { - DeviceViewState({Key? key}) : super(); + DeviceViewState() : super(); List? selectedDevices; @@ -44,7 +42,7 @@ class DeviceViewState extends State { @protected onToggleAllDevices({required List devices}) { - var newSelectedDevices = this.selectedDevices ?? []; + var newSelectedDevices = selectedDevices ?? []; if (newSelectedDevices.length == devices.length) { newSelectedDevices = []; @@ -59,7 +57,7 @@ class DeviceViewState extends State { @protected onToggleModifyDevice({Device? device}) { - var newSelectedDevices = this.selectedDevices ?? []; + final newSelectedDevices = selectedDevices ?? []; if (newSelectedDevices.contains(device)) { newSelectedDevices.remove(device); @@ -83,15 +81,15 @@ class DeviceViewState extends State { Widget buildDeviceOptionsBar({BuildContext? context, Props? props}) { var selfSelectedDevice; - if (this.selectedDevices != null) { - selfSelectedDevice = this.selectedDevices!.indexWhere( - (device) => device!.deviceId == props!.currentDeviceId, - ); + if (selectedDevices != null) { + selfSelectedDevice = selectedDevices!.indexWhere( + (device) => device!.deviceId == props!.currentDeviceId, + ); } return AppBar( brightness: Brightness.dark, // TOOD: this should inherit from theme - backgroundColor: Colors.grey[500], + backgroundColor: Color(Colours.greyDefault), automaticallyImplyLeading: false, titleSpacing: 0.0, title: Row( @@ -114,7 +112,7 @@ class DeviceViewState extends State { iconSize: Dimensions.buttonAppBarSize, tooltip: 'Rename Device', color: Colors.white, - onPressed: this.selectedDevices!.length != 1 ? null : () {}, + onPressed: selectedDevices!.length != 1 ? null : () {}, ), IconButton( icon: Icon(Icons.delete), @@ -125,8 +123,7 @@ class DeviceViewState extends State { ? null : () => props!.onDeleteDevices( context, - this.selectedDevices, - onComplete: () {}, + selectedDevices, ), ), IconButton( @@ -162,17 +159,16 @@ class DeviceViewState extends State { distinct: true, converter: (Store store) => Props.mapStateToProps(store), builder: (context, props) { - final sectionBackgroundColor = - Theme.of(context).brightness == Brightness.dark - ? const Color(Colours.blackDefault) - : const Color(Colours.whiteDefault); + final sectionBackgroundColor = Theme.of(context).brightness == Brightness.dark + ? const Color(Colours.blackDefault) + : const Color(Colours.whiteDefault); var currentAppBar = buildAppBar( props: props, context: context, ); - if (this.selectedDevices != null) { + if (selectedDevices != null) { currentAppBar = buildDeviceOptionsBar( props: props, context: context, @@ -198,81 +194,69 @@ class DeviceViewState extends State { Color? iconColor; Color? backgroundColor; IconData deviceTypeIcon = Icons.phone_android; - TextStyle textStyle = Theme.of(context) - .textTheme - .caption! - .copyWith(fontSize: 12); - bool isCurrentDevice = - props.currentDeviceId == device.deviceId; - - if (device.displayName!.contains('Firefox') || - device.displayName!.contains('Mac')) { + TextStyle textStyle = Theme.of(context).textTheme.caption!.copyWith(fontSize: 12); + final bool isCurrentDevice = props.currentDeviceId == device.deviceId; + + if (device.displayName!.contains('Firefox') || device.displayName!.contains('Mac')) { deviceTypeIcon = Icons.laptop; } else if (device.displayName!.contains('iOS')) { deviceTypeIcon = Icons.phone_iphone; } - if (this.selectedDevices != null && - this.selectedDevices!.contains(device)) { + if (selectedDevices != null && selectedDevices!.contains(device)) { backgroundColor = Colours.hashedColor(device.deviceId); - backgroundColor = Colors.grey[500]; + backgroundColor = Color(Colours.greyDefault); textStyle = textStyle.copyWith(color: Colors.white); iconColor = Colors.white; } return InkWell( - onTap: this.selectedDevices == null - ? null - : () => onToggleModifyDevice(device: device), + onTap: selectedDevices == null ? null : () => onToggleModifyDevice(device: device), onLongPress: () => onToggleModifyDevice(device: device), child: Card( elevation: 0, color: backgroundColor ?? sectionBackgroundColor, - child: Container( - // padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Stack( - children: [ - Container( - padding: - EdgeInsets.only(bottom: 8, top: 8), - child: Icon( - deviceTypeIcon, - size: Dimensions.iconSize * 1.5, - color: iconColor, - ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + children: [ + Container( + padding: EdgeInsets.only(bottom: 8, top: 8), + child: Icon( + deviceTypeIcon, + size: Dimensions.iconSize * 1.5, + color: iconColor, ), - Visibility( - visible: isCurrentDevice, - child: Positioned( - right: 0, - bottom: 4, - child: CircleAvatar( - radius: 8, - backgroundColor: Colors.cyan, - ), + ), + Visibility( + visible: isCurrentDevice, + child: Positioned( + right: 0, + bottom: 4, + child: CircleAvatar( + radius: 8, + backgroundColor: Colors.cyan, ), ), - ], - ), - Column( - children: [ - Text( - device.displayName!, - textAlign: TextAlign.center, - style: textStyle, - ), - Text( - device.deviceId!, - overflow: TextOverflow.ellipsis, - style: textStyle, - ), - ], - ), - ], - ), + ), + ], + ), + Column( + children: [ + Text( + device.displayName!, + textAlign: TextAlign.center, + style: textStyle, + ), + Text( + device.deviceId!, + overflow: TextOverflow.ellipsis, + style: textStyle, + ), + ], + ), + ], ), ), ); @@ -300,7 +284,7 @@ class Props extends Equatable { final Function onFetchDevices; final Function onDeleteDevices; - Props({ + const Props({ required this.loading, required this.devices, required this.session, @@ -320,43 +304,39 @@ class Props extends Equatable { devices: store.state.settingsStore.devices, session: store.state.authStore.session, currentDeviceId: store.state.authStore.user.deviceId, - onDeleteDevices: ( - BuildContext context, - List devices, { - Function? onComplete, - }) async { + onDeleteDevices: (BuildContext context, List devices) async { if (devices.isEmpty) return; - final List deviceIds = - devices.map((device) => device.deviceId).toList(); + final List deviceIds = devices.map((device) => device.deviceId).toList(); if (devices.length == 1) { await store.dispatch(deleteDevice(deviceId: deviceIds[0])); } else { await store.dispatch(deleteDevices(deviceIds: deviceIds)); } + final authSession = store.state.authStore.session; if (authSession != null) { showDialog( context: context, - builder: (context) => DialogConfirmPassword( + builder: (dialogContext) => DialogConfirmPassword( key: Key(authSession), + title: tr(StringIds.titleConfirmPassword), + content: Strings.contentDeleteDevices, onConfirm: () async { - final List deviceIds = - devices.map((device) => device.deviceId).toList(); + final List deviceIds = devices.map((device) => device.deviceId).toList(); if (devices.length == 1) { await store.dispatch(deleteDevice(deviceId: deviceIds[0])); } else { await store.dispatch(deleteDevices(deviceIds: deviceIds)); } - store.dispatch(resetCredentials()); - if (onComplete != null) { - onComplete(); - } + store.dispatch(resetInteractiveAuth()); + Navigator.of(dialogContext).pop(); }, onCancel: () async { - store.dispatch(resetCredentials()); + store.dispatch(resetInteractiveAuth()); + Navigator.of(dialogContext).pop(); }, ), ); diff --git a/lib/views/home/settings/settings-notifications-screen.dart b/lib/views/home/settings/settings-notifications-screen.dart index a8aa7ba17..f05c99245 100644 --- a/lib/views/home/settings/settings-notifications-screen.dart +++ b/lib/views/home/settings/settings-notifications-screen.dart @@ -1,18 +1,14 @@ -// Dart imports: import 'dart:io'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; import 'package:syphon/global/algos.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/notifications.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/home/settings/settings-privacy-screen.dart b/lib/views/home/settings/settings-privacy-screen.dart index 6719885b7..aa14e8670 100644 --- a/lib/views/home/settings/settings-privacy-screen.dart +++ b/lib/views/home/settings/settings-privacy-screen.dart @@ -1,33 +1,108 @@ -// Flutter imports: import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/string-keys.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/global/values.dart'; import 'package:syphon/store/alerts/actions.dart'; +import 'package:syphon/store/auth/actions.dart'; import 'package:syphon/store/crypto/actions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/settings/actions.dart'; import 'package:syphon/views/widgets/containers/card-section.dart'; +import 'package:syphon/views/widgets/dialogs/dialog-confirm-password.dart'; +import 'package:syphon/views/widgets/loader/loading-indicator.dart'; class PrivacySettingsScreen extends StatelessWidget { const PrivacySettingsScreen({Key? key}) : super(key: key); + onConfirmDeactivateAccount({ + required _Props props, + required BuildContext context, + }) async { + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text('Confirm Deactivate Account'), + content: Text(Strings.contentDeactivateAccount), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + child: Text( + Strings.buttonCancel, + ), + ), + TextButton( + onPressed: () async { + Navigator.of(dialogContext).pop(); + props.onResetConfirmAuth(); + onConfirmDeactivateAccountFinal(props: props, context: context); + }, + child: Text( + Strings.buttonConfirm, + style: TextStyle( + color: Colors.redAccent, + ), + ), + ), + ], + ), + ); + } + + onConfirmDeactivateAccountFinal({ + required _Props props, + required BuildContext context, + }) async { + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text('Confirm Deactivate Account'), + content: Text(Strings.contentDeactivateAccountFinal), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + child: Text( + Strings.buttonCancel, + ), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + await props.onDeactivateAccount(context); + }, + child: props.loading + ? LoadingIndicator() + : Text( + Strings.buttonDeactivate, + style: TextStyle( + color: Colors.redAccent, + ), + ), + ), + ], + ), + ); + } + + onConfirmAuth() {} + @override - Widget build(BuildContext context) => StoreConnector( + Widget build(BuildContext context) => StoreConnector( distinct: true, - converter: (Store store) => Props.mapStateToProps(store), + converter: (Store store) => _Props.mapStateToProps(store), builder: (context, props) { - double width = MediaQuery.of(context).size.width; + final double width = MediaQuery.of(context).size.width; return Scaffold( appBar: AppBar( @@ -225,6 +300,35 @@ class PrivacySettingsScreen extends StatelessWidget { ], ), ), + CardSection( + child: Column( + children: [ + Container( + width: width, + padding: Dimensions.listPadding, + child: Text( + 'Account Management', + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.subtitle2, + ), + ), + ListTile( + onTap: () => onConfirmDeactivateAccount( + props: props, + context: context, + ), + contentPadding: Dimensions.listPadding, + title: Text( + 'Deactivate Account', + style: TextStyle( + fontSize: 18.0, + color: Colors.redAccent, + ), + ), + ), + ], + ), + ), ], )), ); @@ -232,9 +336,10 @@ class PrivacySettingsScreen extends StatelessWidget { ); } -class Props extends Equatable { - final bool? typingIndicators; +class _Props extends Equatable { + final bool loading; final bool? readReceipts; + final bool? typingIndicators; final Function onToggleTypingIndicators; final Function onToggleReadReceipts; @@ -242,93 +347,125 @@ class Props extends Equatable { final Function onImportDeviceKey; final Function onDeleteDeviceKey; final Function onDisabled; + final Function onDeactivateAccount; + final Function onResetConfirmAuth; - Props({ - required this.onDisabled, - required this.typingIndicators, + const _Props({ + required this.loading, required this.readReceipts, + required this.typingIndicators, + required this.onDisabled, required this.onToggleTypingIndicators, required this.onToggleReadReceipts, required this.onExportDeviceKey, required this.onImportDeviceKey, required this.onDeleteDeviceKey, + required this.onDeactivateAccount, + required this.onResetConfirmAuth, }); @override List get props => [ + loading, typingIndicators, readReceipts, ]; - static Props mapStateToProps(Store store) => Props( + static _Props mapStateToProps(Store store) => _Props( + loading: store.state.authStore.loading, typingIndicators: store.state.settingsStore.typingIndicatorsEnabled, readReceipts: store.state.settingsStore.readReceiptsEnabled, onDisabled: () => store.dispatch(addInProgress()), + onResetConfirmAuth: () => store.dispatch(resetInteractiveAuth()), + onDeactivateAccount: (BuildContext context) async { + // Attempt to deactivate account + await store.dispatch(deactivateAccount()); + + // Prompt for password if an Interactive Auth sessions was started + final authSession = store.state.authStore.session; + if (authSession != null) { + showDialog( + context: context, + builder: (dialogContext) => DialogConfirmPassword( + key: Key(authSession), + title: tr(StringIds.titleConfirmPassword), + content: tr(StringIds.promptConfirmDeactivation), + onConfirm: () async { + await store.dispatch(deactivateAccount()); + Navigator.of(dialogContext).pop(); + }, + onCancel: () async { + Navigator.of(dialogContext).pop(); + }, + ), + ); + } + }, onToggleTypingIndicators: () => store.dispatch( toggleTypingIndicators(), ), onToggleReadReceipts: () => store.dispatch( toggleReadReceipts(), ), + onImportDeviceKey: () { + store.dispatch(importDeviceKeysOwned()); + }, onExportDeviceKey: (BuildContext context) async { await showDialog( context: context, - builder: (context) => AlertDialog( - title: Text("Confirm Exporting Keys"), + builder: (dialogContext) => AlertDialog( + title: Text('Confirm Exporting Keys'), content: Text(Strings.contentDeleteDeviceKeyWarning), actions: [ TextButton( - child: Text(Strings.buttonCancel), onPressed: () { Navigator.of(context).pop(); }, + child: Text(Strings.buttonCancel), ), TextButton( + onPressed: () async { + store.dispatch(exportDeviceKeysOwned()); + Navigator.of(dialogContext).pop(); + }, child: Text( 'Export Keys', style: TextStyle( color: Colors.redAccent, ), ), - onPressed: () async { - store.dispatch(exportDeviceKeysOwned()); - Navigator.of(context).pop(); - }, ), ], ), ); }, - onImportDeviceKey: () { - store.dispatch(importDeviceKeysOwned()); - }, onDeleteDeviceKey: (BuildContext context) async { await showDialog( context: context, - builder: (context) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: Text(Strings.titleDialogDeleteKeys), content: Text(Strings.confirmationDeleteKeys), actions: [ TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, child: Text( Strings.buttonCancel, style: Theme.of(context).textTheme.subtitle1, ), - onPressed: () { - Navigator.of(context).pop(); - }, ), TextButton( + onPressed: () async { + await store.dispatch(deleteDeviceKeys()); + Navigator.of(dialogContext).pop(); + }, child: Text( Strings.buttonDeleteKeys, style: Theme.of(context).textTheme.subtitle1!.copyWith( color: Colors.redAccent, ), ), - onPressed: () async { - await store.dispatch(deleteDeviceKeys()); - Navigator.of(context).pop(); - }, ), ], ), diff --git a/lib/views/home/settings/settings-screen.dart b/lib/views/home/settings/settings-screen.dart index c75c973a5..bfb16bb3c 100644 --- a/lib/views/home/settings/settings-screen.dart +++ b/lib/views/home/settings/settings-screen.dart @@ -1,14 +1,11 @@ -// Flutter imports: import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/store/alerts/actions.dart'; diff --git a/lib/views/home/settings/settings-storage-screen.dart b/lib/views/home/settings/settings-storage-screen.dart index a1d1c664a..e9441cd24 100644 --- a/lib/views/home/settings/settings-storage-screen.dart +++ b/lib/views/home/settings/settings-storage-screen.dart @@ -1,13 +1,10 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/values.dart'; import 'package:syphon/store/crypto/actions.dart'; diff --git a/lib/views/home/settings/settings-theming-screen.dart b/lib/views/home/settings/settings-theming-screen.dart index 845312791..cade70565 100644 --- a/lib/views/home/settings/settings-theming-screen.dart +++ b/lib/views/home/settings/settings-theming-screen.dart @@ -1,14 +1,11 @@ -// Flutter imports: import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/string-keys.dart'; diff --git a/lib/views/home/settings/widgets/profile-preview.dart b/lib/views/home/settings/widgets/profile-preview.dart index c2f78c9bb..17fc16aad 100644 --- a/lib/views/home/settings/widgets/profile-preview.dart +++ b/lib/views/home/settings/widgets/profile-preview.dart @@ -1,13 +1,10 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/user/model.dart'; diff --git a/lib/views/intro/intro-screen.dart b/lib/views/intro/intro-screen.dart index f707acf52..adbe2513b 100644 --- a/lib/views/intro/intro-screen.dart +++ b/lib/views/intro/intro-screen.dart @@ -1,15 +1,13 @@ -// Dart imports: import 'dart:io'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -// Package imports: + import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -// Project imports: + import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; @@ -73,9 +71,7 @@ class IntroScreenState extends State { double width = MediaQuery.of(context).size.width; if (alphaAgreement == null || true) { - final termsTitle = Platform.isIOS - ? Strings.titleDialogTerms - : Strings.titleDialogTermsAlpha; + final termsTitle = Platform.isIOS ? Strings.titleDialogTerms : Strings.titleDialogTermsAlpha; showDialog( context: context, @@ -142,16 +138,16 @@ class IntroScreenState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ Container( - padding: EdgeInsets.all(16), + padding: EdgeInsets.symmetric(vertical: 16), child: TextButton( - child: Text( - Strings.buttonAgree, - style: TextStyle(color: Theme.of(context).primaryColor), - ), onPressed: () async { await store.dispatch(acceptAgreement()); Navigator.of(context).pop(); }, + child: Text( + Strings.buttonAgree, + style: TextStyle(color: Theme.of(context).primaryColor), + ), ), ), ], @@ -275,8 +271,7 @@ class IntroScreenState extends State { dotWidth: 12, paintStyle: PaintingStyle.fill, strokeWidth: 12, - activeDotColor: - Theme.of(context).primaryColor, + activeDotColor: Theme.of(context).primaryColor, ), // your preferred effect ), ], @@ -303,14 +298,9 @@ class IntroScreenState extends State { child: Text( Strings.buttonIntroExistAction, textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .bodyText2! - .copyWith( - color: - Theme.of(context).primaryColor, - decoration: - TextDecoration.underline, + style: Theme.of(context).textTheme.bodyText2!.copyWith( + color: Theme.of(context).primaryColor, + decoration: TextDecoration.underline, ), ), ), diff --git a/lib/views/intro/login/forgot/password-forgot-screen.dart b/lib/views/intro/login/forgot/password-forgot-screen.dart index 732948e3f..94b3efca4 100644 --- a/lib/views/intro/login/forgot/password-forgot-screen.dart +++ b/lib/views/intro/login/forgot/password-forgot-screen.dart @@ -1,13 +1,10 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/views/behaviors.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; @@ -25,6 +22,7 @@ final Duration nextAnimationDuration = Duration( class ForgotPasswordScreen extends StatefulWidget { const ForgotPasswordScreen({Key? key}) : super(key: key); + @override ForgotPasswordState createState() => ForgotPasswordState(); } @@ -38,9 +36,7 @@ class ForgotPasswordState extends State { EmailVerifyStep(), ]; - ForgotPasswordState({ - Key? key, - }); + ForgotPasswordState(); @override void initState() { diff --git a/lib/views/intro/login/forgot/password-reset-screen.dart b/lib/views/intro/login/forgot/password-reset-screen.dart index 3d05071ec..d21092566 100644 --- a/lib/views/intro/login/forgot/password-reset-screen.dart +++ b/lib/views/intro/login/forgot/password-reset-screen.dart @@ -1,12 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/views/behaviors.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; @@ -23,6 +21,7 @@ final Duration nextAnimationDuration = Duration( class ResetPasswordScreen extends StatefulWidget { const ResetPasswordScreen({Key? key}) : super(key: key); + @override PasswordResetState createState() => PasswordResetState(); } @@ -37,9 +36,7 @@ class PasswordResetState extends State { PasswordResetStep(), ]; - PasswordResetState({ - Key? key, - }); + PasswordResetState(); @override void initState() { @@ -106,7 +103,6 @@ class PasswordResetState extends State { allowImplicitScrolling: false, controller: pageController, physics: NeverScrollableScrollPhysics(), - children: sections, onPageChanged: (index) { setState(() { currentStep = index; @@ -114,6 +110,7 @@ class PasswordResetState extends State { index != sections.length - 1; }); }, + children: sections, ), ), ], @@ -168,13 +165,20 @@ class _Props extends Equatable { final Map interactiveAuths; final Function onResetPassword; - _Props({ + const _Props({ required this.loading, required this.isPasswordValid, required this.interactiveAuths, required this.onResetPassword, }); + @override + List get props => [ + loading, + isPasswordValid, + interactiveAuths, + ]; + static _Props mapStateToProps(Store store) => _Props( loading: store.state.authStore.loading, isPasswordValid: store.state.authStore.isPasswordValid, @@ -185,11 +189,4 @@ class _Props extends Equatable { ); }, ); - - @override - List get props => [ - loading, - isPasswordValid, - interactiveAuths, - ]; } diff --git a/lib/views/intro/login/forgot/widgets/PageEmailVerify.dart b/lib/views/intro/login/forgot/widgets/PageEmailVerify.dart index b17ba46a0..666a4caf6 100644 --- a/lib/views/intro/login/forgot/widgets/PageEmailVerify.dart +++ b/lib/views/intro/login/forgot/widgets/PageEmailVerify.dart @@ -1,18 +1,14 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter/services.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/intro/login/forgot/widgets/PagePasswordReset.dart b/lib/views/intro/login/forgot/widgets/PagePasswordReset.dart index 2d4fd5653..c01091891 100644 --- a/lib/views/intro/login/forgot/widgets/PagePasswordReset.dart +++ b/lib/views/intro/login/forgot/widgets/PagePasswordReset.dart @@ -1,17 +1,15 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; +import 'package:syphon/global/strings.dart'; import 'package:syphon/store/auth/actions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/views/widgets/input/text-field-secure.dart'; @@ -62,143 +60,137 @@ class PasswordResetStepState extends State { builder: (context, props) { double width = MediaQuery.of(context).size.width; - return Container( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Flexible( - flex: 4, - fit: FlexFit.tight, - child: Container( - width: width * 0.65, - padding: EdgeInsets.only(bottom: 8), - constraints: BoxConstraints( - maxHeight: Dimensions.mediaSizeMax, - maxWidth: Dimensions.mediaSizeMax, - ), - child: SvgPicture.asset( - Assets.heroSignupPassword, - semanticsLabel: - 'User thinking up a password in a swirl of wind', - ), + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + flex: 4, + fit: FlexFit.tight, + child: Container( + width: width * 0.65, + padding: EdgeInsets.only(bottom: 8), + constraints: BoxConstraints( + maxHeight: Dimensions.mediaSizeMax, + maxWidth: Dimensions.mediaSizeMax, + ), + child: SvgPicture.asset( + Assets.heroSignupPassword, + semanticsLabel: + 'User thinking up a password in a swirl of wind', ), ), - Flexible( - flex: 2, - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.only(bottom: 8, top: 8), - child: Text( - 'Come up with 4 random words\nyou\'ll easily remember', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption, - ), + ), + Flexible( + flex: 2, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.only(bottom: 8, top: 8), + child: Text( + Strings.passwordRecommendationDefault, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.caption, ), - Container( - padding: EdgeInsets.symmetric(vertical: 8), - child: Text( - 'Create a password', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headline5, - ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 8), + child: Text( + 'Create a password', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline5, ), - ], - ), + ), + ], ), - Flexible( - flex: 1, - child: Container( - child: TextFieldSecure( - label: 'Password', - focusNode: passwordFocusNode, - controller: passwordController, - obscureText: !visibility, - onChanged: (text) { - props.onChangePassword(text); - }, - onSubmitted: (String value) { - FocusScope.of(context).requestFocus(confirmFocusNode); - }, - onEditingComplete: () { - FocusScope.of(context).requestFocus(confirmFocusNode); - }, - suffix: GestureDetector( - onTap: () { - if (!passwordFocusNode.hasFocus) { - // Unfocus all focus nodes - passwordFocusNode.unfocus(); - - // Disable text field's focus node request - passwordFocusNode.canRequestFocus = false; - } - - // Do your stuff - setState(() { - visibility = !this.visibility; - }); - - if (!passwordFocusNode.hasFocus) { - //Enable the text field's focus node request after some delay - Future.delayed(Duration(milliseconds: 100), () { - passwordFocusNode.canRequestFocus = true; - }); - } - }, - child: Icon( - visibility ? Icons.visibility : Icons.visibility_off, - ), - ), + ), + Flexible( + flex: 1, + child: TextFieldSecure( + label: 'Password', + focusNode: passwordFocusNode, + controller: passwordController, + obscureText: !visibility, + onChanged: (text) { + props.onChangePassword(text); + }, + onSubmitted: (String value) { + FocusScope.of(context).requestFocus(confirmFocusNode); + }, + onEditingComplete: () { + FocusScope.of(context).requestFocus(confirmFocusNode); + }, + suffix: GestureDetector( + onTap: () { + if (!passwordFocusNode.hasFocus) { + // Unfocus all focus nodes + passwordFocusNode.unfocus(); + + // Disable text field's focus node request + passwordFocusNode.canRequestFocus = false; + } + + // Do your stuff + setState(() { + visibility = !visibility; + }); + + if (!passwordFocusNode.hasFocus) { + //Enable the text field's focus node request after some delay + Future.delayed(Duration(milliseconds: 100), () { + passwordFocusNode.canRequestFocus = true; + }); + } + }, + child: Icon( + visibility ? Icons.visibility : Icons.visibility_off, ), ), ), - Container( - padding: EdgeInsets.symmetric( - vertical: 8, - )), - Flexible( - flex: 1, - child: Container( - child: TextFieldSecure( - label: 'Confirm Password', - focusNode: confirmFocusNode, - controller: confirmController, - obscureText: !visibility, - onChanged: (text) { - props.onChangePasswordConfirm(text); - }, - onSubmitted: (String value) { - confirmFocusNode.unfocus(); - }, - onEditingComplete: () { - props.onChangePasswordConfirm(props.passwordConfirm); - }, - suffix: Visibility( - visible: props.isPasswordValid, - child: Container( - width: 12, - height: 12, - margin: EdgeInsets.all(6), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(24), - ), - child: Container( - padding: EdgeInsets.all((6)), - child: Icon( - Icons.check, - color: Colors.white, - ), - ), + ), + Container( + padding: EdgeInsets.symmetric( + vertical: 8, + )), + Flexible( + flex: 1, + child: TextFieldSecure( + label: 'Confirm Password', + focusNode: confirmFocusNode, + controller: confirmController, + obscureText: !visibility, + onChanged: (text) { + props.onChangePasswordConfirm(text); + }, + onSubmitted: (String value) { + confirmFocusNode.unfocus(); + }, + onEditingComplete: () { + props.onChangePasswordConfirm(props.passwordConfirm); + }, + suffix: Visibility( + visible: props.isPasswordValid, + child: Container( + width: 12, + height: 12, + margin: EdgeInsets.all(6), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(24), + ), + child: Container( + padding: EdgeInsets.all((6)), + child: Icon( + Icons.check, + color: Colors.white, ), ), ), ), ), - ], - ), + ), + ], ); }, ); @@ -212,7 +204,7 @@ class _Props extends Equatable { final Function onChangePassword; final Function onChangePasswordConfirm; - _Props({ + const _Props({ required this.password, required this.passwordConfirm, required this.isPasswordValid, diff --git a/lib/views/intro/login/login-screen.dart b/lib/views/intro/login/login-screen.dart index c9556fa19..4dd1c0c37 100644 --- a/lib/views/intro/login/login-screen.dart +++ b/lib/views/intro/login/login-screen.dart @@ -1,12 +1,9 @@ -// Dart imports: import 'dart:async'; import 'dart:math'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter/services.dart'; @@ -18,7 +15,6 @@ import 'package:syphon/store/auth/homeserver/model.dart'; import 'package:syphon/views/widgets/avatars/avatar.dart'; import 'package:touchable_opacity/touchable_opacity.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/views/behaviors.dart'; import 'package:syphon/global/dimensions.dart'; @@ -171,7 +167,7 @@ class LoginScreenState extends State { // Do your stuff setState(() { - visibility = !this.visibility; + visibility = !visibility; }); if (!passwordFocus.hasFocus) { @@ -313,9 +309,8 @@ class LoginScreenState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ Visibility( - visible: - props.loginType == MatrixAuthTypes.PASSWORD || - props.loginType == MatrixAuthTypes.DUMMY, + visible: props.loginType == MatrixAuthTypes.PASSWORD || + props.loginType == MatrixAuthTypes.DUMMY, child: buildPasswordLogin(props), ), Visibility( @@ -337,8 +332,7 @@ class LoginScreenState extends State { child: Stack( children: [ Visibility( - visible: props.loginType == - MatrixAuthTypes.PASSWORD || + visible: props.loginType == MatrixAuthTypes.PASSWORD || props.loginType == MatrixAuthTypes.DUMMY, child: ButtonSolid( text: Strings.buttonLogin, @@ -348,8 +342,7 @@ class LoginScreenState extends State { ), ), Visibility( - visible: - props.loginType == MatrixAuthTypes.SSO, + visible: props.loginType == MatrixAuthTypes.SSO, child: ButtonSolid( text: Strings.buttonLoginSSO, loading: props.loading, @@ -390,10 +383,7 @@ class LoginScreenState extends State { child: Text( Strings.buttonLoginCreateAction, textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .bodyText2! - .copyWith( + style: Theme.of(context).textTheme.bodyText2!.copyWith( color: Theme.of(context).primaryColor, decoration: TextDecoration.underline, ), @@ -431,7 +421,7 @@ class _Props extends Equatable { final Function onChangeHomeserver; final Function onResetSession; - _Props({ + const _Props({ required this.loading, required this.username, required this.password, @@ -463,18 +453,17 @@ class _Props extends Equatable { password: store.state.authStore.password, homeserver: store.state.authStore.homeserver, loginType: store.state.authStore.homeserver.loginType, - isLoginAttemptable: - store.state.authStore.homeserver.loginType == MatrixAuthTypes.SSO || - (store.state.authStore.isPasswordValid && - store.state.authStore.isUsernameValid && - !store.state.authStore.loading && - !store.state.authStore.stopgap), + isLoginAttemptable: store.state.authStore.homeserver.loginType == MatrixAuthTypes.SSO || + (store.state.authStore.isPasswordValid && + store.state.authStore.isUsernameValid && + !store.state.authStore.loading && + !store.state.authStore.stopgap), usernameHint: Strings.formatUsernameHint( username: store.state.authStore.username, homeserver: store.state.authStore.hostname, ), onResetSession: () async { - await store.dispatch(resetSession()); + await store.dispatch(resetInteractiveAuth()); }, onChangeUsername: (String text) async { await store.dispatch(resolveUsername(username: text)); diff --git a/lib/views/intro/search/search-homeserver-screen.dart b/lib/views/intro/search/search-homeserver-screen.dart index 8ad8fec05..91d1ad16f 100644 --- a/lib/views/intro/search/search-homeserver-screen.dart +++ b/lib/views/intro/search/search-homeserver-screen.dart @@ -1,12 +1,9 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:expandable/expandable.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -18,7 +15,6 @@ import 'package:syphon/store/auth/homeserver/model.dart'; import 'package:syphon/views/widgets/appbars/appbar-search.dart'; import 'package:syphon/views/widgets/avatars/avatar.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/auth/actions.dart'; import 'package:syphon/store/index.dart'; @@ -33,11 +29,11 @@ class SearchHomeserverScreen extends StatefulWidget { } class SearchHomeserverScreenState extends State { - final Store? store; final searchInputFocusNode = FocusNode(); bool searching = false; - SearchHomeserverScreenState({Key? key, this.store}); + ExpandableController controller = ExpandableController(); + SearchHomeserverScreenState(); @override void didChangeDependencies() { @@ -49,7 +45,7 @@ class SearchHomeserverScreenState extends State { void onMounted() { final store = StoreProvider.of(context); - if (!this.searching) { + if (!searching) { setState(() { searching = true; }); @@ -101,8 +97,8 @@ class SearchHomeserverScreenState extends State { scrollDirection: Axis.vertical, itemCount: props.homeservers.length, itemBuilder: (BuildContext context, int index) { - final Homeserver homeserver = - props.homeservers[index] ?? Map() as Homeserver; + final homeserver = + props.homeservers[index] ?? {} as Homeserver; return GestureDetector( onTap: () { @@ -115,7 +111,10 @@ class SearchHomeserverScreenState extends State { theme: ExpandableThemeData( hasIcon: false, tapBodyToCollapse: true, - tapHeaderToExpand: true, + tapHeaderToExpand: false, + expandIcon: IconData( + Icons.arrow_drop_down.codePoint, + ), ), header: ListTile( leading: Avatar( @@ -220,7 +219,7 @@ class SearchHomeserverScreenState extends State { softWrap: true, ), Text( - homeserver.responseTime! + 'ms', + '${homeserver.responseTime ?? '0'}ms', softWrap: true, ), ], @@ -240,7 +239,7 @@ class SearchHomeserverScreenState extends State { ), Visibility( visible: props.searchText.isNotEmpty && - props.searchText.length > 0 && + props.searchText.isNotEmpty && props.homeservers.isEmpty, child: Container( padding: EdgeInsets.only(top: 8, bottom: 8), @@ -256,7 +255,7 @@ class SearchHomeserverScreenState extends State { url: props.homeserver.photoUrl != null ? props.homeserver.photoUrl : null, - background: props.searchText.length > 0 + background: props.searchText.isNotEmpty ? Colours.hashedColor(props.searchText) : Colors.grey, ), @@ -292,7 +291,7 @@ class _Props extends Equatable { final Function onSelect; final Function onFetchHomeserverPreview; - _Props({ + const _Props({ required this.loading, required this.homeservers, required this.searchText, @@ -325,7 +324,7 @@ class _Props extends Equatable { store.dispatch(searchHomeservers(searchText: text)); }, onFetchHomeserverPreview: (String hostname) async { - var urlRegex = new RegExp(Values.urlRegex, caseSensitive: false); + final urlRegex = RegExp(Values.urlRegex, caseSensitive: false); if (urlRegex.hasMatch('https://$hostname')) { final preview = await store.dispatch( diff --git a/lib/views/intro/signup/loading-screen.dart b/lib/views/intro/signup/loading-screen.dart index 07f2e7699..eff94832e 100644 --- a/lib/views/intro/signup/loading-screen.dart +++ b/lib/views/intro/signup/loading-screen.dart @@ -1,12 +1,9 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:flutter_redux/flutter_redux.dart'; import 'package:touchable_opacity/touchable_opacity.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/views/behaviors.dart'; import 'package:syphon/store/index.dart'; diff --git a/lib/views/intro/signup/signup-screen.dart b/lib/views/intro/signup/signup-screen.dart index 044b60da2..0aa65d4e0 100644 --- a/lib/views/intro/signup/signup-screen.dart +++ b/lib/views/intro/signup/signup-screen.dart @@ -1,17 +1,13 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -// Project imports: import 'package:syphon/views/behaviors.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/libs/matrix/auth.dart'; @@ -29,7 +25,6 @@ import 'widgets/StepHomeserver.dart'; import 'widgets/StepPassword.dart'; import 'widgets/StepUsername.dart'; -// Styling Widgets final Duration nextAnimationDuration = Duration( milliseconds: Values.animationDurationDefault, ); @@ -37,6 +32,7 @@ final Duration nextAnimationDuration = Duration( class SignupScreen extends StatefulWidget { const SignupScreen({Key? key}) : super(key: key); + @override SignupScreenState createState() => SignupScreenState(); } @@ -53,7 +49,7 @@ class SignupScreenState extends State { PasswordStep(), ]; - SignupScreenState({Key? key}); + SignupScreenState(); @override void initState() { @@ -71,26 +67,30 @@ class SignupScreenState extends State { onMounted(); } - @protected - void onMounted() async { + @override + void dispose() { + subscription.cancel(); + super.dispose(); + } + + onMounted() async { final store = StoreProvider.of(context); final props = _Props.mapStateToProps(store); if (props.homeserver.loginType == MatrixAuthTypes.SSO) { setState(() { - sections = sections - ..removeWhere((step) => step.runtimeType != HomeserverStep); + sections = sections..removeWhere((step) => step.runtimeType != HomeserverStep); }); } // Init change listener subscription = store.onChange.listen((state) async { - if (state.authStore.interactiveAuths.isNotEmpty && - this.sections.length < 4) { + if (state.authStore.interactiveAuths.isNotEmpty && sections.length < 4) { final newSections = List.from(sections); List? newStages = []; + try { newStages = state.authStore.interactiveAuths['flows'][0]['stages']; } catch (error) { @@ -121,12 +121,10 @@ class SignupScreenState extends State { }); } - @protected - void onDidChange(_Props? oldProps, _Props props) { + onDidChange(_Props? oldProps, _Props props) { if (props.homeserver.loginType == MatrixAuthTypes.SSO) { setState(() { - sections = sections - ..removeWhere((step) => step.runtimeType != HomeserverStep); + sections = sections..removeWhere((step) => step.runtimeType != HomeserverStep); }); } if (props.homeserver.loginType == MatrixAuthTypes.PASSWORD) { @@ -140,22 +138,15 @@ class SignupScreenState extends State { } } - @override - void deactivate() { - subscription.cancel(); - super.deactivate(); - } - - @protected - void onBackStep(BuildContext context) { - if (this.currentStep < 1) { + onBackStep(BuildContext context) { + if (currentStep < 1) { Navigator.pop(context, false); } else { setState(() { - currentStep = this.currentStep - 1; + currentStep = currentStep - 1; }); pageController!.animateToPage( - this.currentStep, + currentStep, duration: Duration(milliseconds: 275), curve: Curves.easeInOut, ); @@ -164,15 +155,13 @@ class SignupScreenState extends State { @protected bool? onCheckStepValid(_Props props, PageController? controller) { - final currentSection = this.sections[this.currentStep]; + final currentSection = sections[currentStep]; switch (currentSection.runtimeType) { case HomeserverStep: return props.isHomeserverValid; case UsernameStep: - return props.isUsernameValid && - props.isUsernameAvailable && - !props.loading; + return props.isUsernameValid && props.isUsernameAvailable && !props.loading; case PasswordStep: return props.isPasswordValid; case EmailStep: @@ -188,8 +177,8 @@ class SignupScreenState extends State { @protected Function? onCompleteStep(_Props props, PageController? controller) { - final currentSection = this.sections[this.currentStep]; - final lastStep = (this.sections.length - 1) == this.currentStep; + final currentSection = sections[currentStep]; + final lastStep = (sections.length - 1) == currentStep; switch (currentSection.runtimeType) { case HomeserverStep: return () async { @@ -221,7 +210,7 @@ class SignupScreenState extends State { case PasswordStep: return () async { if (sections.length < 4) { - final result = await props.onCreateUser(); + final result = await props.onCreateUser(enableErrors: lastStep); // If signup is completed here, just wait for auth redirect if (result) { @@ -229,7 +218,7 @@ class SignupScreenState extends State { } } - return await controller!.nextPage( + return controller!.nextPage( duration: nextAnimationDuration, curve: Curves.ease, ); @@ -306,7 +295,7 @@ class SignupScreenState extends State { return Strings.buttonLoginSSO; } - if (this.currentStep == sections.length - 1) { + if (currentStep == sections.length - 1) { return Strings.buttonSignupFinish; } @@ -319,8 +308,8 @@ class SignupScreenState extends State { onDidChange: onDidChange, converter: (Store store) => _Props.mapStateToProps(store), builder: (context, props) { - double width = MediaQuery.of(context).size.width; - double height = MediaQuery.of(context).size.height; + final double width = MediaQuery.of(context).size.width; + final double height = MediaQuery.of(context).size.height; return Scaffold( extendBodyBehindAppBar: true, @@ -342,10 +331,8 @@ class SignupScreenState extends State { behavior: DefaultScrollBehavior(), child: SingleChildScrollView( child: Container( - width: - width, // set actual height and width for flex constraints - height: - height, // set actual height and width for flex constraints + width: width, + height: height, child: Flex( direction: Axis.vertical, mainAxisAlignment: MainAxisAlignment.center, @@ -369,14 +356,13 @@ class SignupScreenState extends State { allowImplicitScrolling: false, controller: pageController, physics: NeverScrollableScrollPhysics(), - children: sections, onPageChanged: (index) { setState(() { currentStep = index; - onboarding = index != 0 && - index != sections.length - 1; + onboarding = index != 0 && index != sections.length - 1; }); }, + children: sections, ), ), ], @@ -388,19 +374,17 @@ class SignupScreenState extends State { mainAxisAlignment: MainAxisAlignment.end, direction: Axis.vertical, children: [ - Container( - child: ButtonSolid( - text: buildButtonString(props), - loading: props.creating || props.loading, - disabled: props.creating || - !onCheckStepValid( - props, - this.pageController, - )!, - onPressed: onCompleteStep( - props, - this.pageController, - ), + ButtonSolid( + text: buildButtonString(props), + loading: props.creating || props.loading, + disabled: props.creating || + !onCheckStepValid( + props, + pageController, + )!, + onPressed: onCompleteStep( + props, + pageController, ), ), ], @@ -421,16 +405,14 @@ class SignupScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ SmoothPageIndicator( - controller: - pageController!, // PageController + controller: pageController!, count: sections.length, effect: WormEffect( spacing: 16, dotHeight: 12, dotWidth: 12, - activeDotColor: - Theme.of(context).primaryColor, - ), // your preferred effect + activeDotColor: Theme.of(context).primaryColor, + ), ), ], ), @@ -481,7 +463,7 @@ class _Props extends Equatable { final Function onResetCredential; final Function onSelectHomeserver; - _Props({ + const _Props({ required this.user, required this.hostname, required this.homeserver, @@ -512,8 +494,7 @@ class _Props extends Equatable { completed: store.state.authStore.completed, hostname: store.state.authStore.hostname, homeserver: store.state.authStore.homeserver, - isHomeserverValid: store.state.authStore.homeserver.valid && - !store.state.authStore.loading, + isHomeserverValid: store.state.authStore.homeserver.valid && !store.state.authStore.loading, username: store.state.authStore.username, isUsernameValid: store.state.authStore.isUsernameValid, isUsernameAvailable: store.state.authStore.isUsernameAvailable, diff --git a/lib/views/intro/signup/verification-screen.dart b/lib/views/intro/signup/verification-screen.dart index 71c8970b9..1f7361e73 100644 --- a/lib/views/intro/signup/verification-screen.dart +++ b/lib/views/intro/signup/verification-screen.dart @@ -1,14 +1,11 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/views/behaviors.dart'; import 'package:syphon/global/dimensions.dart'; @@ -51,7 +48,7 @@ class VerificationScreenState extends State } @override - void didChangeAppLifecycleState(AppLifecycleState state) async { + didChangeAppLifecycleState(AppLifecycleState state) async { final store = StoreProvider.of(context); final props = _Props.mapStateToProps(store); @@ -74,8 +71,8 @@ class VerificationScreenState extends State distinct: true, converter: (Store store) => _Props.mapStateToProps(store), builder: (context, props) { - double width = MediaQuery.of(context).size.width; - double height = MediaQuery.of(context).size.height; + final double width = MediaQuery.of(context).size.width; + final double height = MediaQuery.of(context).size.height; return Scaffold( body: ScrollConfiguration( @@ -189,14 +186,14 @@ class VerificationScreenState extends State ), child: ButtonSolid( text: 'resend email', - loading: this.sending || props.loading, - disabled: this.sending || props.loading, + loading: sending || props.loading, + disabled: sending || props.loading, onPressed: () { props.onResendVerification( - sendAttempt: this.sendAttempt + 1, + sendAttempt: sendAttempt + 1, ); setState(() { - sendAttempt = this.sendAttempt + 1; + sendAttempt = sendAttempt + 1; }); }, ), @@ -211,7 +208,7 @@ class VerificationScreenState extends State ), child: ButtonText( text: 'check verification', - disabled: this.sending || props.loading, + disabled: sending || props.loading, onPressed: () async { final result = await props.onCreateUser( enableErrors: true); @@ -242,7 +239,7 @@ class _Props extends Equatable { final Function onCreateUser; final Function onResendVerification; - _Props({ + const _Props({ required this.loading, required this.verification, required this.onCreateUser, diff --git a/lib/views/intro/signup/widgets/StepCaptcha.dart b/lib/views/intro/signup/widgets/StepCaptcha.dart index d2a78e45d..2e64c9c30 100644 --- a/lib/views/intro/signup/widgets/StepCaptcha.dart +++ b/lib/views/intro/signup/widgets/StepCaptcha.dart @@ -1,16 +1,14 @@ -// Flutter imports: import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; +import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/views/widgets/buttons/button-text.dart'; @@ -19,6 +17,7 @@ import 'package:syphon/views/widgets/dialogs/dialog-captcha.dart'; class CaptchaStep extends StatefulWidget { const CaptchaStep({Key? key}) : super(key: key); + @override CaptchaStepState createState() => CaptchaStepState(); } @@ -35,103 +34,101 @@ class CaptchaStepState extends State { distinct: true, converter: (Store store) => _Props.mapStateToProps(store), builder: (context, props) { - double width = MediaQuery.of(context).size.width; + final double width = MediaQuery.of(context).size.width; - return Container( - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible( - flex: 6, - child: Container( - width: width * 0.75, - constraints: BoxConstraints( - maxHeight: Dimensions.mediaSizeMax, - maxWidth: Dimensions.mediaSizeMax, - ), - child: SvgPicture.asset( - Assets.heroAcceptTerms, - semanticsLabel: tr('semantics-image-terms-of-service'), - ), + return Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + flex: 6, + child: Container( + width: width * 0.75, + constraints: BoxConstraints( + maxHeight: Dimensions.mediaSizeMax, + maxWidth: Dimensions.mediaSizeMax, + ), + child: SvgPicture.asset( + Assets.heroAcceptTerms, + semanticsLabel: tr('semantics-image-terms-of-service'), ), ), - Flexible( - flex: 2, - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.only(bottom: 8, top: 8), - child: Text( - tr('content-signup-captcha-requirement'), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption, - ), + ), + Flexible( + flex: 2, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.only(bottom: 8, top: 8), + child: Text( + tr('content-signup-captcha-requirement'), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.caption, ), - Container( - child: Stack( - overflow: Overflow.visible, - children: [ - Container( - padding: EdgeInsets.symmetric( - vertical: 8, - horizontal: 24, - ), - child: Text( - 'Confirm you\'re alive', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headline5, - ), - ), - Positioned( - top: 0, - right: 0, - child: GestureDetector( - onTap: () { - // TODO: show captcha explaination dialog - }, - child: Container( - height: 20, - width: 20, - child: Icon( - Icons.info_outline, - color: Colors.white, - size: 20, - ), - ), + ), + Stack( + clipBehavior: Clip.none, + children: [ + Container( + padding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 24, + ), + child: Text( + 'Confirm you\'re alive', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline5, + ), + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () { + // TODO: show captcha explaination dialog + }, + child: Container( + height: 20, + width: 20, + child: Icon( + Icons.info_outline, + color: Colors.white, + size: 20, ), ), - ], + ), ), - ), - ], - ), + ], + ), + ], ), - Flexible( - flex: 1, - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ButtonText( - text: props.completed - ? tr('button-text-confirmed') - : tr('button-text-load-captcha'), - color: props.completed ? Color(0xff49c489) : null, - loading: props.loading, - disabled: props.completed, - onPressed: () => props.onShowCaptcha( - context, - ), + ), + Flexible( + flex: 1, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ButtonText( + text: !props.completed + ? tr('button-text-load-captcha') + : tr('button-text-confirmed'), + color: !props.completed + ? Color(Colours.cyanSyphon) + : Color(Colours.cyanSyphonAlpha), + loading: props.loading, + disabled: props.completed, + onPressed: () => props.onShowCaptcha( + context, ), - ], - ), + ), + ], ), - ], - ), + ), + ], ); }); } @@ -142,7 +139,7 @@ class _Props extends Equatable { final Function onShowCaptcha; - _Props({ + const _Props({ required this.loading, required this.completed, required this.onShowCaptcha, @@ -151,9 +148,7 @@ class _Props extends Equatable { static _Props mapStateToProps(Store store) => _Props( loading: store.state.authStore.loading, completed: store.state.authStore.captcha, - onShowCaptcha: ( - BuildContext context, - ) async { + onShowCaptcha: (BuildContext context) async { final authSession = store.state.authStore.session; await showDialog( context: context, diff --git a/lib/views/intro/signup/widgets/StepEmail.dart b/lib/views/intro/signup/widgets/StepEmail.dart index dbad94a3e..081a30940 100644 --- a/lib/views/intro/signup/widgets/StepEmail.dart +++ b/lib/views/intro/signup/widgets/StepEmail.dart @@ -1,17 +1,13 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/libs/matrix/auth.dart'; @@ -54,7 +50,7 @@ class EmailStepState extends State { distinct: true, converter: (Store store) => _Props.mapStateToProps(store), builder: (context, props) { - double height = MediaQuery.of(context).size.height; + final double height = MediaQuery.of(context).size.height; Color suffixBackgroundColor = Colors.grey; Widget suffixWidget = CircularProgressIndicator( @@ -117,51 +113,49 @@ class EmailStepState extends State { style: Theme.of(context).textTheme.caption, ), ), - Container( - child: Stack( - overflow: Overflow.visible, - children: [ - Container( - padding: EdgeInsets.symmetric( - vertical: 8, - horizontal: 24, - ), - child: Text( - 'Enter an email address', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headline5, - ), + Stack( + clipBehavior: Clip.none, + children: [ + Container( + padding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 24, + ), + child: Text( + 'Enter an email address', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline5, ), - Positioned( - top: 0, - right: 0, - child: GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (BuildContext context) => - DialogExplaination( - title: Strings.titleDialogEmailRequirement, - content: Strings.contentEmailRequirement, - onConfirm: () { - Navigator.pop(context); - }, - ), - ); - }, - child: Container( - height: 20, - width: 20, - child: Icon( - Icons.info_outline, - color: Theme.of(context).accentColor, - size: 20, + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) => + DialogExplaination( + title: Strings.titleDialogEmailRequirement, + content: Strings.contentEmailRequirement, + onConfirm: () { + Navigator.pop(context); + }, ), + ); + }, + child: Container( + height: 20, + width: 20, + child: Icon( + Icons.info_outline, + color: Theme.of(context).accentColor, + size: 20, ), ), ), - ], - ), + ), + ], ), Visibility( visible: !props.isEmailAvailable, diff --git a/lib/views/intro/signup/widgets/StepHomeserver.dart b/lib/views/intro/signup/widgets/StepHomeserver.dart index a7ba0512a..12cbc9d7e 100644 --- a/lib/views/intro/signup/widgets/StepHomeserver.dart +++ b/lib/views/intro/signup/widgets/StepHomeserver.dart @@ -1,14 +1,11 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; @@ -27,7 +24,7 @@ class HomeserverStep extends StatefulWidget { } class HomeserverStepState extends State { - HomeserverStepState({Key? key}); + HomeserverStepState(); final homeserverController = TextEditingController(); @@ -184,7 +181,7 @@ class _Props extends Equatable { final Function onSetHostname; final Function onChangeHomeserver; - _Props({ + const _Props({ required this.hostname, required this.homeserver, required this.onSetHostname, diff --git a/lib/views/intro/signup/widgets/StepPassword.dart b/lib/views/intro/signup/widgets/StepPassword.dart index fd14acbcb..bf3b9a89e 100644 --- a/lib/views/intro/signup/widgets/StepPassword.dart +++ b/lib/views/intro/signup/widgets/StepPassword.dart @@ -1,25 +1,20 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; +import 'package:syphon/global/strings.dart'; import 'package:syphon/store/auth/actions.dart'; import 'package:syphon/store/index.dart'; +import 'package:syphon/views/widgets/dialogs/dialog-container.dart'; +import 'package:syphon/views/widgets/dialogs/dialog-explaination.dart'; import 'package:syphon/views/widgets/input/text-field-secure.dart'; -// Store - -// Styling - class PasswordStep extends StatefulWidget { const PasswordStep({Key? key}) : super(key: key); @@ -27,7 +22,7 @@ class PasswordStep extends StatefulWidget { } class PasswordStepState extends State { - PasswordStepState({Key? key}); + PasswordStepState(); bool visibility = false; @@ -59,150 +54,181 @@ class PasswordStepState extends State { super.dispose(); } + onCheckInfo(BuildContext context) async { + await showDialog( + context: context, + builder: (BuildContext context) => DialogExplaination( + title: 'Password Requirements', + content: Strings.contentPasswordRequirements, + onConfirm: () { + Navigator.pop(context); + }, + ), + ); + } + @override Widget build(BuildContext context) => StoreConnector( distinct: true, converter: (Store store) => _Props.mapStateToProps(store), builder: (context, props) { - double width = MediaQuery.of(context).size.width; - - return Container( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Flexible( - flex: 4, - fit: FlexFit.tight, - child: Container( - width: width * 0.65, - padding: EdgeInsets.only(bottom: 8), - constraints: BoxConstraints( - maxHeight: Dimensions.mediaSizeMax, - maxWidth: Dimensions.mediaSizeMax, - ), - child: SvgPicture.asset( - Assets.heroSignupPassword, - semanticsLabel: - 'User thinking up a password in a swirl of wind', - ), + final double width = MediaQuery.of(context).size.width; + + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + flex: 4, + fit: FlexFit.tight, + child: Container( + width: width * 0.65, + padding: EdgeInsets.only(bottom: 8), + constraints: BoxConstraints( + maxHeight: Dimensions.mediaSizeMax, + maxWidth: Dimensions.mediaSizeMax, ), - ), - Flexible( - flex: 2, - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.only(bottom: 8, top: 8), - child: Text( - 'Come up with 4 random words\nyou\'ll easily remember', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption, - ), - ), - Container( - padding: EdgeInsets.symmetric(vertical: 8), - child: Text( - 'Create a password', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headline5, - ), - ), - ], + child: SvgPicture.asset( + Assets.heroSignupPassword, + semanticsLabel: + 'User thinking up a password in a swirl of wind', ), ), - Flexible( - flex: 1, - child: Container( - child: TextFieldSecure( - label: 'Password', - focusNode: passwordFocusNode, - controller: passwordController, - obscureText: !visibility, - onChanged: (text) { - props.onChangePassword(text); - }, - onSubmitted: (String value) { - FocusScope.of(context).requestFocus(confirmFocusNode); - }, - onEditingComplete: () { - FocusScope.of(context).requestFocus(confirmFocusNode); - }, - suffix: GestureDetector( - onTap: () { - if (!passwordFocusNode.hasFocus) { - // Unfocus all focus nodes - passwordFocusNode.unfocus(); - - // Disable text field's focus node request - passwordFocusNode.canRequestFocus = false; - } - - // Do your stuff - setState(() { - visibility = !this.visibility; - }); - - if (!passwordFocusNode.hasFocus) { - //Enable the text field's focus node request after some delay - Future.delayed(Duration(milliseconds: 100), () { - passwordFocusNode.canRequestFocus = true; - }); - } - }, - child: Icon( - visibility ? Icons.visibility : Icons.visibility_off, - ), + ), + Flexible( + flex: 2, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.only(bottom: 8, top: 8), + child: Text( + Strings.passwordRecommendationDefault, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.caption, ), ), - ), - ), - Container( - padding: EdgeInsets.symmetric( - vertical: 8, - )), - Flexible( - flex: 1, - child: Container( - child: TextFieldSecure( - label: 'Confirm Password', - focusNode: confirmFocusNode, - controller: confirmController, - obscureText: !visibility, - onChanged: (text) { - props.onChangePasswordConfirm(text); - }, - onSubmitted: (String value) { - confirmFocusNode.unfocus(); - }, - onEditingComplete: () { - props.onChangePasswordConfirm(props.passwordConfirm); - }, - suffix: Visibility( - visible: props.isPasswordValid, - child: Container( - width: 12, - height: 12, - margin: EdgeInsets.all(6), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(24), + Stack( + clipBehavior: Clip.none, + children: [ + Container( + padding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 24, ), - child: Container( - padding: EdgeInsets.all((6)), - child: Icon( - Icons.check, - color: Colors.white, + child: Text( + 'Create a password', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline5, + ), + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => onCheckInfo(context), + child: Container( + height: 20, + width: 20, + child: Icon( + Icons.info_outline, + color: Theme.of(context).accentColor, + size: 20, + ), ), ), ), + ], + ), + ], + ), + ), + Flexible( + flex: 1, + child: TextFieldSecure( + label: 'Password', + focusNode: passwordFocusNode, + controller: passwordController, + obscureText: !visibility, + onChanged: (text) { + props.onChangePassword(text); + }, + onSubmitted: (String value) { + FocusScope.of(context).requestFocus(confirmFocusNode); + }, + onEditingComplete: () { + FocusScope.of(context).requestFocus(confirmFocusNode); + }, + suffix: GestureDetector( + onTap: () { + if (!passwordFocusNode.hasFocus) { + // Unfocus all focus nodes + passwordFocusNode.unfocus(); + + // Disable text field's focus node request + passwordFocusNode.canRequestFocus = false; + } + + // Do your stuff + setState(() { + visibility = !visibility; + }); + + if (!passwordFocusNode.hasFocus) { + //Enable the text field's focus node request after some delay + Future.delayed(Duration(milliseconds: 100), () { + passwordFocusNode.canRequestFocus = true; + }); + } + }, + child: Icon( + visibility ? Icons.visibility : Icons.visibility_off, + ), + ), + ), + ), + Container( + padding: EdgeInsets.symmetric( + vertical: 8, + )), + Flexible( + flex: 1, + child: TextFieldSecure( + label: 'Confirm Password', + focusNode: confirmFocusNode, + controller: confirmController, + obscureText: !visibility, + onChanged: (text) { + props.onChangePasswordConfirm(text); + }, + onSubmitted: (String value) { + confirmFocusNode.unfocus(); + }, + onEditingComplete: () { + props.onChangePasswordConfirm(props.passwordConfirm); + }, + suffix: Visibility( + visible: props.isPasswordValid, + child: Container( + width: 12, + height: 12, + margin: EdgeInsets.all(6), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(24), + ), + child: Container( + padding: EdgeInsets.all(6), + child: Icon( + Icons.check, + color: Colors.white, + ), ), ), ), ), - ], - ), + ), + ], ); }, ); @@ -216,7 +242,7 @@ class _Props extends Equatable { final Function onChangePassword; final Function onChangePasswordConfirm; - _Props({ + const _Props({ required this.password, required this.passwordConfirm, required this.isPasswordValid, diff --git a/lib/views/intro/signup/widgets/StepTerms.dart b/lib/views/intro/signup/widgets/StepTerms.dart index 5165f5329..1471301ae 100644 --- a/lib/views/intro/signup/widgets/StepTerms.dart +++ b/lib/views/intro/signup/widgets/StepTerms.dart @@ -1,15 +1,12 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:redux/redux.dart'; import 'package:url_launcher/url_launcher.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; diff --git a/lib/views/intro/signup/widgets/StepUsername.dart b/lib/views/intro/signup/widgets/StepUsername.dart index 39afe20fd..6e98094d3 100644 --- a/lib/views/intro/signup/widgets/StepUsername.dart +++ b/lib/views/intro/signup/widgets/StepUsername.dart @@ -1,18 +1,14 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/auth/actions.dart'; @@ -53,7 +49,7 @@ class UsernameStepState extends State { distinct: true, converter: (Store store) => _Props.mapStateToProps(store), builder: (context, props) { - double height = MediaQuery.of(context).size.height; + final double height = MediaQuery.of(context).size.height; Color suffixBackgroundColor = Colors.grey; Widget suffixWidget = CircularProgressIndicator( diff --git a/lib/views/intro/widgets/page-action.dart b/lib/views/intro/widgets/page-action.dart index 66ee75204..63943ee2e 100644 --- a/lib/views/intro/widgets/page-action.dart +++ b/lib/views/intro/widgets/page-action.dart @@ -1,11 +1,8 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:flutter_svg/flutter_svg.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/intro/widgets/page-description-first.dart b/lib/views/intro/widgets/page-description-first.dart index cb73bfc2a..52fc5e3fd 100644 --- a/lib/views/intro/widgets/page-description-first.dart +++ b/lib/views/intro/widgets/page-description-first.dart @@ -1,11 +1,8 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:flutter_svg/flutter_svg.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/intro/widgets/page-description-second.dart b/lib/views/intro/widgets/page-description-second.dart index ba25e2780..73d240e71 100644 --- a/lib/views/intro/widgets/page-description-second.dart +++ b/lib/views/intro/widgets/page-description-second.dart @@ -1,11 +1,8 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:flutter_svg/flutter_svg.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/intro/widgets/page-description-third.dart b/lib/views/intro/widgets/page-description-third.dart index 550cdfec0..89de412ff 100644 --- a/lib/views/intro/widgets/page-description-third.dart +++ b/lib/views/intro/widgets/page-description-third.dart @@ -1,11 +1,8 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:flutter_svg/flutter_svg.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/intro/widgets/page-landing.dart b/lib/views/intro/widgets/page-landing.dart index f19b61624..e5ffbe342 100644 --- a/lib/views/intro/widgets/page-landing.dart +++ b/lib/views/intro/widgets/page-landing.dart @@ -2,10 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:flutter_svg/flutter_svg.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; diff --git a/lib/views/navigation.dart b/lib/views/navigation.dart index 6871af0d3..2b00032b8 100644 --- a/lib/views/navigation.dart +++ b/lib/views/navigation.dart @@ -1,11 +1,9 @@ -// Flutter imports: import 'package:flutter/material.dart'; -// Project imports: import 'package:syphon/global/values.dart'; -import 'package:syphon/views/home/chat/details-all-users-screen.dart'; -import 'package:syphon/views/home/chat/details-chat-screen.dart'; -import 'package:syphon/views/home/chat/details-message-screen.dart'; +import 'package:syphon/views/home/chat/chat-detail-all-users-screen.dart'; +import 'package:syphon/views/home/chat/chat-detail-screen.dart'; +import 'package:syphon/views/home/chat/chat-detail-message-screen.dart'; import 'package:syphon/views/home/chat/chat-screen.dart'; import 'package:syphon/views/home/groups/group-create-public-screen.dart'; import 'package:syphon/views/home/groups/group-create-screen.dart'; @@ -18,7 +16,7 @@ import 'package:syphon/views/home/search/search-rooms-screen.dart'; import 'package:syphon/views/home/search/search-users-screen.dart'; import 'package:syphon/views/home/settings/advanced-settings-screen.dart'; import 'package:syphon/views/home/settings/blocked-screen.dart'; -import 'package:syphon/views/home/settings/settings-chat-screen.dart'; +import 'package:syphon/views/home/settings/settings-chats-screen.dart'; import 'package:syphon/views/home/settings/settings-devices-screen.dart'; import 'package:syphon/views/home/settings/settings-screen.dart'; import 'package:syphon/views/home/settings/settings-notifications-screen.dart'; @@ -36,29 +34,25 @@ import 'package:syphon/views/intro/signup/loading-screen.dart'; import 'package:syphon/views/intro/signup/verification-screen.dart'; class NavigationService { - static final GlobalKey navigatorKey = - GlobalKey(); + static final GlobalKey navigatorKey = GlobalKey(); static Future navigateTo(String routeName) { return navigatorKey.currentState!.pushNamed(routeName); } static Future clearTo(String routeName, BuildContext context) { - return navigatorKey.currentState! - .pushNamedAndRemoveUntil(routeName, (_) => false); + return navigatorKey.currentState!.pushNamedAndRemoveUntil(routeName, (_) => false); } } class NavigationProvider { - static Map getRoutes() => - { + static Map getRoutes() => { '/intro': (BuildContext context) => const IntroScreen(), '/login': (BuildContext context) => const LoginScreen(), '/signup': (BuildContext context) => const SignupScreen(), '/forgot': (BuildContext context) => ForgotPasswordScreen(), '/reset': (BuildContext context) => ResetPasswordScreen(), - '/search/homeservers': (BuildContext context) => - SearchHomeserverScreen(), + '/search/homeservers': (BuildContext context) => SearchHomeserverScreen(), '/verification': (BuildContext context) => VerificationScreen(), '/home': (BuildContext context) => HomeScreen(), '/home/chat': (BuildContext context) => ChatScreen(), @@ -71,18 +65,15 @@ class NavigationProvider { '/home/rooms/search': (BuildContext context) => RoomSearchScreen(), '/home/groups/search': (BuildContext context) => GroupSearchScreen(), '/home/groups/create': (BuildContext context) => CreateGroupScreen(), - '/home/groups/create/public': (BuildContext context) => - CreatePublicGroupScreen(), + '/home/groups/create/public': (BuildContext context) => CreatePublicGroupScreen(), '/profile': (BuildContext context) => ProfileScreen(), - '/notifications': (BuildContext context) => - NotificationSettingsScreen(), + '/notifications': (BuildContext context) => NotificationSettingsScreen(), '/advanced': (BuildContext context) => AdvancedSettingsScreen(), '/storage': (BuildContext context) => StorageSettingsScreen(), '/password': (BuildContext context) => PasswordUpdateView(), - '/licenses': (BuildContext context) => - LicensePage(applicationName: Values.appName), + '/licenses': (BuildContext context) => LicensePage(applicationName: Values.appName), '/privacy': (BuildContext context) => PrivacySettingsScreen(), - '/chat-preferences': (BuildContext context) => ChatSettingsScreen(), + '/chat-preferences': (BuildContext context) => ChatsSettingsScreen(), '/theming': (BuildContext context) => ThemingSettingsScreen(), '/devices': (BuildContext context) => DevicesScreen(), '/settings': (BuildContext context) => SettingsScreen(), diff --git a/lib/views/widgets/appbars/appbar-chat.dart b/lib/views/widgets/appbars/appbar-chat.dart index e24967c63..6b866618e 100644 --- a/lib/views/widgets/appbars/appbar-chat.dart +++ b/lib/views/widgets/appbars/appbar-chat.dart @@ -1,4 +1,3 @@ -// Flutter imports: import 'dart:async'; import 'package:equatable/equatable.dart'; @@ -15,11 +14,9 @@ import 'package:syphon/store/rooms/room/selectors.dart'; import 'package:syphon/store/settings/notification-settings/actions.dart'; import 'package:syphon/store/user/actions.dart'; import 'package:syphon/store/user/model.dart'; -import 'package:syphon/views/home/chat/details-chat-screen.dart'; -import 'package:syphon/views/home/chat/chat-screen.dart'; +import 'package:syphon/views/home/chat/chat-detail-screen.dart'; import 'package:syphon/views/home/groups/invite-users-screen.dart'; import 'package:syphon/views/widgets/avatars/avatar.dart'; -import 'package:syphon/views/widgets/buttons/button-text.dart'; import 'package:syphon/views/widgets/containers/menu-rounded.dart'; import 'package:syphon/views/widgets/dialogs/dialog-confirm.dart'; import 'package:syphon/views/widgets/dialogs/dialog-container.dart'; @@ -223,8 +220,7 @@ class AppBarChatState extends State { @override Widget build(BuildContext context) => StoreConnector( distinct: true, - converter: (Store store) => - _Props.mapStateToProps(store, widget.room!.id), + converter: (Store store) => _Props.mapStateToProps(store, widget.room!.id), builder: (context, props) => AppBar( titleSpacing: 0.0, automaticallyImplyLeading: false, @@ -243,7 +239,7 @@ class AppBarChatState extends State { Navigator.pushNamed( context, '/home/chat/settings', - arguments: ChatSettingsArguments( + arguments: ChatDetailArguments( roomId: widget.room!.id, title: widget.room!.name, ), @@ -285,9 +281,7 @@ class AppBarChatState extends State { ), ), Visibility( - visible: widget.badgesEnabled && - widget.room!.type == 'group' && - !widget.room!.invite, + visible: widget.badgesEnabled && widget.room!.type == 'group' && !widget.room!.invite, child: Positioned( right: 0, bottom: 0, @@ -307,9 +301,8 @@ class AppBarChatState extends State { ), ), Visibility( - visible: widget.badgesEnabled && - widget.room!.type == 'public' && - !widget.room!.invite, + visible: + widget.badgesEnabled && widget.room!.type == 'public' && !widget.room!.invite, child: Positioned( right: 0, bottom: 0, @@ -336,10 +329,7 @@ class AppBarChatState extends State { child: Text( widget.room!.name!, overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1! - .copyWith(color: Colors.white), + style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Colors.white), ), ), ], @@ -373,7 +363,7 @@ class AppBarChatState extends State { Navigator.pushNamed( context, '/home/chat/settings', - arguments: ChatSettingsArguments( + arguments: ChatDetailArguments( roomId: widget.room!.id, title: widget.room!.name, ), @@ -449,8 +439,7 @@ class _Props extends Equatable { @override List get props => []; - static _Props mapStateToProps(Store store, String? roomId) => - _Props( + static _Props mapStateToProps(Store store, String? roomId) => _Props( currentUser: store.state.authStore.user, roomUsers: (store.state.roomStore.rooms[roomId!]!.userIds) .map((id) => store.state.userStore.users[id]) @@ -466,8 +455,7 @@ class _Props extends Equatable { )); }, onToggleNotifications: () { - store.dispatch( - toggleChatNotifications(roomId: roomId, enabled: false)); + store.dispatch(toggleChatNotifications(roomId: roomId, enabled: false)); }, ); } diff --git a/lib/views/widgets/appbars/appbar-options-message.dart b/lib/views/widgets/appbars/appbar-options-message.dart index aeb99d58e..9b03baceb 100644 --- a/lib/views/widgets/appbars/appbar-options-message.dart +++ b/lib/views/widgets/appbars/appbar-options-message.dart @@ -1,17 +1,16 @@ -// Flutter imports: import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:syphon/global/colours.dart'; import 'package:syphon/store/events/messages/model.dart'; import 'package:syphon/store/events/selectors.dart'; import 'package:syphon/store/rooms/room/model.dart'; -import 'package:syphon/views/home/chat/details-message-screen.dart'; +import 'package:syphon/views/home/chat/chat-detail-message-screen.dart'; -class AppBarMessageOptions extends StatefulWidget - implements PreferredSizeWidget { - AppBarMessageOptions({ +class AppBarMessageOptions extends StatefulWidget implements PreferredSizeWidget { + const AppBarMessageOptions({ Key? key, this.title = 'title:', this.label = 'label:', @@ -61,7 +60,7 @@ class AppBarMessageOptionState extends State { @override Widget build(BuildContext context) => AppBar( brightness: Brightness.dark, // TOOD: this should inherit from theme - backgroundColor: Colors.grey[500], + backgroundColor: Color(Colours.greyDefault), automaticallyImplyLeading: false, titleSpacing: 0.0, title: Row( diff --git a/lib/views/widgets/appbars/appbar-search.dart b/lib/views/widgets/appbars/appbar-search.dart index b065b7336..064f8417d 100644 --- a/lib/views/widgets/appbars/appbar-search.dart +++ b/lib/views/widgets/appbars/appbar-search.dart @@ -1,11 +1,9 @@ -// Flutter imports: import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -// Project imports: import 'package:touchable_opacity/touchable_opacity.dart'; class AppBarSearch extends StatefulWidget implements PreferredSizeWidget { diff --git a/lib/views/widgets/avatars/avatar-app-bar.dart b/lib/views/widgets/avatars/avatar-app-bar.dart index d5733118f..30eb0369b 100644 --- a/lib/views/widgets/avatars/avatar-app-bar.dart +++ b/lib/views/widgets/avatars/avatar-app-bar.dart @@ -1,8 +1,6 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/themes.dart'; import 'package:syphon/store/user/model.dart'; diff --git a/lib/views/widgets/avatars/avatar.dart b/lib/views/widgets/avatars/avatar.dart index 2335652ff..f82e826ff 100644 --- a/lib/views/widgets/avatars/avatar.dart +++ b/lib/views/widgets/avatars/avatar.dart @@ -1,12 +1,11 @@ -// Flutter imports: import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; +import 'package:syphon/global/formatters.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/user/selectors.dart'; import 'package:syphon/views/widgets/image-matrix.dart'; @@ -40,8 +39,8 @@ class Avatar extends StatelessWidget { distinct: true, converter: (Store store) => _Props.mapStateToProps(store), builder: (context, props) { - final Color backgroundColor = - uri != null || url != null ? Colors.transparent : Colors.grey; + final bool emptyAvi = uri == null && url == null; + final Color backgroundColor = !emptyAvi ? Colors.transparent : Colors.grey; var borderRadius = BorderRadius.circular(size); @@ -52,7 +51,7 @@ class Avatar extends StatelessWidget { Widget avatarWidget = ClipRRect( borderRadius: borderRadius, child: Text( - formatInitials(alt ?? ''), + formatInitialsLong(alt ?? ''), style: TextStyle( color: Colors.white, fontSize: Dimensions.avatarFontSize(size: size), @@ -98,10 +97,9 @@ class Avatar extends StatelessWidget { width: size, height: size, decoration: BoxDecoration( - borderRadius: borderRadius, - color: uri == null && url == null && !force - ? background ?? backgroundColor - : Colors.transparent), + borderRadius: borderRadius, + color: emptyAvi && !force ? background ?? backgroundColor : Colors.transparent, + ), child: Center(child: avatarWidget), ), Visibility( @@ -116,8 +114,7 @@ class Avatar extends StatelessWidget { border: Border.all( color: Colors.white, ), - borderRadius: - BorderRadius.circular(Dimensions.badgeAvatarSize), + borderRadius: BorderRadius.circular(Dimensions.badgeAvatarSize), ), width: Dimensions.badgeAvatarSize, height: Dimensions.badgeAvatarSize, @@ -148,6 +145,5 @@ class _Props extends Equatable { @override List get props => [avatarShape]; - _Props.mapStateToProps(Store store) - : avatarShape = store.state.settingsStore.avatarShape; + _Props.mapStateToProps(Store store) : avatarShape = store.state.settingsStore.avatarShape; } diff --git a/lib/views/widgets/buttons/button-outline.dart b/lib/views/widgets/buttons/button-outline.dart index 5c7bdd589..961873a1f 100644 --- a/lib/views/widgets/buttons/button-outline.dart +++ b/lib/views/widgets/buttons/button-outline.dart @@ -1,12 +1,12 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:syphon/global/colours.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; +import 'package:syphon/views/widgets/loader/loading-indicator.dart'; class ButtonOutline extends StatelessWidget { - ButtonOutline({ + const ButtonOutline({ Key? key, this.text, this.loading = false, @@ -36,14 +36,12 @@ class ButtonOutline extends StatelessWidget { child: TextButton( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.disabled) - ? Colors.grey[300] - : Theme.of(context).primaryColor, + (Set states) => states.contains(MaterialState.disabled) + ? Color(Colours.greyLight) + : Theme.of(context).primaryColor, ), backgroundColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.disabled) ? Colors.grey : null, + (Set states) => states.contains(MaterialState.disabled) ? Colors.grey : null, ), shape: MaterialStateProperty.resolveWith( (Set states) => RoundedRectangleBorder( @@ -51,30 +49,18 @@ class ButtonOutline extends StatelessWidget { ), ), ), - onPressed: disabled ? null : this.onPressed as void Function()?, - child: this.loading - ? Container( - constraints: BoxConstraints( - maxHeight: 28, - maxWidth: 28, - ), - child: CircularProgressIndicator( - strokeWidth: Dimensions.defaultStrokeWidth, - backgroundColor: Colors.white, - valueColor: AlwaysStoppedAnimation( - Colors.grey, - ), - ), - ) + onPressed: disabled ? null : onPressed as void Function()?, + child: loading + ? LoadingIndicator() : (child != null ? child! : Text( - this.text!, + text!, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w100, letterSpacing: 0.8, - color: disabled ? Colors.grey[300] : Colors.white, + color: disabled ? Color(Colours.greyLight) : Colors.white, ), )), ), diff --git a/lib/views/widgets/buttons/button-solid.dart b/lib/views/widgets/buttons/button-solid.dart index 565c336bb..0aa4a1197 100644 --- a/lib/views/widgets/buttons/button-solid.dart +++ b/lib/views/widgets/buttons/button-solid.dart @@ -1,12 +1,12 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:syphon/global/colours.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; +import 'package:syphon/views/widgets/loader/loading-indicator.dart'; class ButtonSolid extends StatelessWidget { - ButtonSolid({ + const ButtonSolid({ Key? key, this.text, this.textWidget, @@ -36,16 +36,13 @@ class ButtonSolid extends StatelessWidget { child: TextButton( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith( - (Set states) => - states.contains(MaterialState.disabled) - ? Colors.grey[300] - : Theme.of(context).primaryColor, + (Set states) => states.contains(MaterialState.disabled) + ? Color(Colours.greyLight) + : Theme.of(context).primaryColor, ), backgroundColor: MaterialStateProperty.resolveWith( (Set states) => - states.contains(MaterialState.disabled) - ? Colors.grey - : Theme.of(context).primaryColor, + states.contains(MaterialState.disabled) ? Colors.grey : Theme.of(context).primaryColor, ), shape: MaterialStateProperty.resolveWith( (Set states) => RoundedRectangleBorder( @@ -53,30 +50,18 @@ class ButtonSolid extends StatelessWidget { ), ), ), - onPressed: disabled ? null : this.onPressed as void Function()?, - child: this.loading - ? Container( - constraints: BoxConstraints( - maxHeight: 28, - maxWidth: 28, - ), - child: CircularProgressIndicator( - strokeWidth: Dimensions.defaultStrokeWidth, - backgroundColor: Colors.white, - valueColor: AlwaysStoppedAnimation( - Colors.grey, - ), - ), - ) + onPressed: disabled ? null : onPressed as void Function()?, + child: loading + ? LoadingIndicator() : (textWidget != null ? textWidget! : Text( - this.text!, + text!, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w100, letterSpacing: 0.8, - color: disabled ? Colors.grey[300] : Colors.white, + color: disabled ? Color(Colours.greyLight) : Colors.white, ), )), ), diff --git a/lib/views/widgets/buttons/button-text-opacity.dart b/lib/views/widgets/buttons/button-text-opacity.dart index 11809093e..c3f4f67e3 100644 --- a/lib/views/widgets/buttons/button-text-opacity.dart +++ b/lib/views/widgets/buttons/button-text-opacity.dart @@ -1,8 +1,7 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:syphon/global/colours.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; class ButtonTextOpacity extends StatefulWidget { @@ -64,26 +63,25 @@ class ButtonTextState extends State { ), ), ) - : (widget.textWidget != null - ? widget.textWidget - : Text( - widget.text!, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w100, - letterSpacing: 0.8, - color: () { - if (widget.disabled) { - return Colors.grey[300]; - } - if (widget.color != null) { - return widget.color; - } - return Theme.of(context).textTheme.button!.color; - }(), - ), - )), + : (widget.textWidget ?? + Text( + widget.text!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w100, + letterSpacing: 0.8, + color: () { + if (widget.disabled) { + return Color(Colours.greyLight); + } + if (widget.color != null) { + return widget.color; + } + return Theme.of(context).textTheme.button!.color; + }(), + ), + )), ), ); } diff --git a/lib/views/widgets/buttons/button-text.dart b/lib/views/widgets/buttons/button-text.dart index 172bd1c36..8d94d7ad5 100644 --- a/lib/views/widgets/buttons/button-text.dart +++ b/lib/views/widgets/buttons/button-text.dart @@ -1,8 +1,7 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:syphon/global/colours.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; class ButtonText extends StatelessWidget { @@ -29,7 +28,7 @@ class ButtonText extends StatelessWidget { foregroundColor: MaterialStateProperty.resolveWith( (Set states) => states.contains(MaterialState.disabled) - ? Colors.grey[300] + ? Color(Colours.greyDisabled) : null, ), ), @@ -53,17 +52,17 @@ class ButtonText extends StatelessWidget { : Text( text!, style: TextStyle( - fontSize: 20, + fontSize: Theme.of(context).textTheme.bodyText1?.fontSize, fontWeight: FontWeight.w100, letterSpacing: 0.8, color: () { if (disabled) { - return Colors.grey[300]; + return Color(Colours.greyDisabled); } if (color != null) { return color; } - return Theme.of(context).buttonColor; + return Theme.of(context).textTheme.bodyText1?.color; }(), ), )), diff --git a/lib/views/widgets/captcha.dart b/lib/views/widgets/captcha.dart index 09e1e4cf5..efbf2cb55 100644 --- a/lib/views/widgets/captcha.dart +++ b/lib/views/widgets/captcha.dart @@ -1,15 +1,11 @@ -// Dart imports: import 'dart:async'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -// Package imports: import 'package:webview_flutter/webview_flutter.dart'; -// Project imports: import 'package:syphon/global/values.dart'; /* @@ -65,7 +61,6 @@ class CaptchaState extends State { } CaptchaState({ - Key? key, this.publickey, this.onVerified, }); @@ -73,30 +68,28 @@ class CaptchaState extends State { // Matrix Public Key @override Widget build(BuildContext context) { - final captchaUrl = '${Values.captchaUrl}${this.publickey}'; + final captchaUrl = '${Values.captchaUrl}$publickey'; - return Container( - child: WebView( - initialUrl: captchaUrl, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: [ - JavascriptChannel( - name: 'RecaptchaFlutterChannel', - onMessageReceived: (JavascriptMessage receiver) { - String token = receiver.message; - if (token.contains("verify")) { - token = token.substring(7); - } - if (this.onVerified != null) { - this.onVerified!(token); - } - }, - ), - ].toSet(), - onWebViewCreated: (WebViewController webViewController) { - controller.complete(webViewController); - }, - ), + return WebView( + initialUrl: captchaUrl, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'RecaptchaFlutterChannel', + onMessageReceived: (JavascriptMessage receiver) { + String token = receiver.message; + if (token.contains('verify')) { + token = token.substring(7); + } + if (onVerified != null) { + onVerified!(token); + } + }, + ), + }, + onWebViewCreated: (WebViewController webViewController) { + controller.complete(webViewController); + }, ); } } diff --git a/lib/views/widgets/containers/card-section.dart b/lib/views/widgets/containers/card-section.dart index 5e9f4709c..1810dacc5 100644 --- a/lib/views/widgets/containers/card-section.dart +++ b/lib/views/widgets/containers/card-section.dart @@ -1,13 +1,10 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/themes.dart'; import 'package:syphon/store/index.dart'; diff --git a/lib/views/widgets/containers/menu-rounded.dart b/lib/views/widgets/containers/menu-rounded.dart index 5d17144e1..42cd40f80 100644 --- a/lib/views/widgets/containers/menu-rounded.dart +++ b/lib/views/widgets/containers/menu-rounded.dart @@ -1,4 +1,3 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/views/widgets/containers/ring-actions.dart b/lib/views/widgets/containers/ring-actions.dart index e0a9eb0ef..f327edf91 100644 --- a/lib/views/widgets/containers/ring-actions.dart +++ b/lib/views/widgets/containers/ring-actions.dart @@ -1,15 +1,12 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:fab_circular_menu/fab_circular_menu.dart'; import 'package:flutter_svg/svg.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/assets.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/themes.dart'; diff --git a/lib/views/widgets/dialogs/dialog-captcha.dart b/lib/views/widgets/dialogs/dialog-captcha.dart index 237cb4374..ea39d71f0 100644 --- a/lib/views/widgets/dialogs/dialog-captcha.dart +++ b/lib/views/widgets/dialogs/dialog-captcha.dart @@ -1,13 +1,10 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/libs/matrix/auth.dart'; import 'package:syphon/global/strings.dart'; @@ -17,7 +14,7 @@ import 'package:syphon/views/widgets/buttons/button-text.dart'; import 'package:syphon/views/widgets/captcha.dart'; class DialogCaptcha extends StatelessWidget { - DialogCaptcha({ + const DialogCaptcha({ Key? key, this.onConfirm, this.onCancel, @@ -31,8 +28,8 @@ class DialogCaptcha extends StatelessWidget { distinct: true, converter: (Store store) => Props.mapStateToProps(store), builder: (context, props) { - double width = MediaQuery.of(context).size.width; - double height = MediaQuery.of(context).size.height; + final double width = MediaQuery.of(context).size.width; + final double height = MediaQuery.of(context).size.height; return SimpleDialog( shape: RoundedRectangleBorder( @@ -77,8 +74,8 @@ class DialogCaptcha extends StatelessWidget { ButtonText( text: 'Cancel', onPressed: () { - if (this.onCancel != null) { - this.onCancel!(); + if (onCancel != null) { + onCancel!(); } Navigator.of(context).pop(); }, @@ -96,19 +93,26 @@ class Props extends Equatable { final Function onCompleteCaptcha; - Props({ + const Props({ required this.completed, required this.publicKey, required this.onCompleteCaptcha, }); + @override + List get props => [ + completed, + publicKey, + ]; + static Props mapStateToProps(Store store) => Props( completed: store.state.authStore.captcha, publicKey: () { return store.state.authStore.interactiveAuths['params'] [MatrixAuthTypes.RECAPTCHA]['public_key']; }(), - onCompleteCaptcha: (String token, {required BuildContext context}) async { + onCompleteCaptcha: (String token, + {required BuildContext context}) async { await store.dispatch(updateCredential( type: MatrixAuthTypes.RECAPTCHA, value: token.toString(), @@ -117,10 +121,4 @@ class Props extends Equatable { Navigator.of(context).pop(); }, ); - - @override - List get props => [ - completed, - publicKey, - ]; } diff --git a/lib/views/widgets/dialogs/dialog-color-picker.dart b/lib/views/widgets/dialogs/dialog-color-picker.dart index 6f2fb5c76..e1ea31ed7 100644 --- a/lib/views/widgets/dialogs/dialog-color-picker.dart +++ b/lib/views/widgets/dialogs/dialog-color-picker.dart @@ -1,11 +1,7 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -// Package imports: - -// Project imports: import 'package:syphon/global/colours.dart'; class DialogColorPicker extends StatelessWidget { diff --git a/lib/views/widgets/dialogs/dialog-confirm-password.dart b/lib/views/widgets/dialogs/dialog-confirm-password.dart index adfef9b79..0c576b923 100644 --- a/lib/views/widgets/dialogs/dialog-confirm-password.dart +++ b/lib/views/widgets/dialogs/dialog-confirm-password.dart @@ -1,27 +1,29 @@ -// Flutter imports: -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; +import 'package:syphon/global/colours.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/store/auth/actions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/settings/devices-settings/model.dart'; +import 'package:syphon/views/widgets/loader/loading-indicator.dart'; class DialogConfirmPassword extends StatelessWidget { const DialogConfirmPassword({ Key? key, + this.title = 'Confirm Password (Default)', + this.content = 'Please confirm your password (Default)', this.onConfirm, this.onCancel, }) : super(key: key); + final String title; + final String content; final Function? onConfirm; final Function? onCancel; @@ -48,9 +50,7 @@ class DialogConfirmPassword extends StatelessWidget { right: 16, bottom: 16, ), - title: Text( - tr('title-delete-devices'), - ), + title: Text(title), children: [ Column( children: [ @@ -62,7 +62,7 @@ class DialogConfirmPassword extends StatelessWidget { left: 8, ), child: Text( - Strings.contentDeleteDevices, + content, textAlign: TextAlign.start, style: Theme.of(context).textTheme.caption, ), @@ -106,10 +106,11 @@ class DialogConfirmPassword extends StatelessWidget { if (onCancel != null) { onCancel!(); } - Navigator.of(context).pop(); } : null, - child: Text('Cancel'), + child: Text( + Strings.buttonCancel, + ), ), TextButton( onPressed: !props.valid @@ -118,23 +119,15 @@ class DialogConfirmPassword extends StatelessWidget { if (onConfirm != null) { onConfirm!(); } - Navigator.of(context).pop(); }, child: !props.loading - ? Text('Confirm') - : Container( - constraints: BoxConstraints( - maxHeight: 16, - maxWidth: 16, - ), - child: CircularProgressIndicator( - strokeWidth: Dimensions.defaultStrokeWidth, - backgroundColor: Colors.white, - valueColor: AlwaysStoppedAnimation( - Colors.grey, - ), - ), - ), + ? Text(Strings.buttonConfirmOfficial, + style: TextStyle( + color: props.valid + ? Theme.of(context).primaryColor + : Color(Colours.greyDisabled), + )) + : LoadingIndicator(), ), ], ) @@ -150,7 +143,7 @@ class Props extends Equatable { final Function onChangePassword; - Props({ + const Props({ required this.valid, required this.loading, required this.devices, @@ -168,8 +161,9 @@ class Props extends Equatable { Store store, ) => Props( - valid: store.state.authStore.credential!.value != null && - store.state.authStore.credential!.value!.length > 0, + valid: store.state.authStore.credential != null && + store.state.authStore.credential!.value != null && + store.state.authStore.credential!.value!.isNotEmpty, loading: store.state.settingsStore.loading, devices: store.state.settingsStore.devices, onChangePassword: (password) { diff --git a/lib/views/widgets/dialogs/dialog-confirm.dart b/lib/views/widgets/dialogs/dialog-confirm.dart index 02d3221cd..6163d6ffd 100644 --- a/lib/views/widgets/dialogs/dialog-confirm.dart +++ b/lib/views/widgets/dialogs/dialog-confirm.dart @@ -1,11 +1,8 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; -import 'package:syphon/store/user/model.dart'; import 'package:syphon/views/widgets/buttons/button-text.dart'; class DialogConfirm extends StatelessWidget { diff --git a/lib/views/widgets/dialogs/dialog-container.dart b/lib/views/widgets/dialogs/dialog-container.dart index 9e8c86971..63e0df3fe 100644 --- a/lib/views/widgets/dialogs/dialog-container.dart +++ b/lib/views/widgets/dialogs/dialog-container.dart @@ -1,4 +1,3 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/views/widgets/dialogs/dialog-explaination.dart b/lib/views/widgets/dialogs/dialog-explaination.dart index 1452a0df6..be95b92f9 100644 --- a/lib/views/widgets/dialogs/dialog-explaination.dart +++ b/lib/views/widgets/dialogs/dialog-explaination.dart @@ -1,15 +1,13 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/store/user/model.dart'; import 'package:syphon/views/widgets/buttons/button-text.dart'; class DialogExplaination extends StatelessWidget { - DialogExplaination({ + const DialogExplaination({ Key? key, this.user, this.title = '', diff --git a/lib/views/widgets/dialogs/dialog-invite-users.dart b/lib/views/widgets/dialogs/dialog-invite-users.dart index cf31b57c5..0ad1f0fa4 100644 --- a/lib/views/widgets/dialogs/dialog-invite-users.dart +++ b/lib/views/widgets/dialogs/dialog-invite-users.dart @@ -1,12 +1,9 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/auth.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/store/auth/actions.dart'; @@ -15,7 +12,7 @@ import 'package:syphon/store/user/model.dart'; import 'package:syphon/views/widgets/buttons/button-text.dart'; class DialogInviteUsers extends StatelessWidget { - DialogInviteUsers({ + const DialogInviteUsers({ Key? key, this.users, this.title = 'Invite Users', @@ -107,11 +104,9 @@ class Props extends Equatable { static Props mapStateToProps(Store store) => Props( completed: store.state.authStore.captcha, publicKey: () { - return store.state.authStore.interactiveAuths['params'] - [MatrixAuthTypes.RECAPTCHA]['public_key']; + return store.state.authStore.interactiveAuths['params'][MatrixAuthTypes.RECAPTCHA]['public_key']; }(), - onCompleteCaptcha: (String token, - {required BuildContext context}) async { + onCompleteCaptcha: (String token, {required BuildContext context}) async { await store.dispatch(updateCredential( type: MatrixAuthTypes.RECAPTCHA, value: token.toString(), diff --git a/lib/views/widgets/dialogs/dialog-start-chat.dart b/lib/views/widgets/dialogs/dialog-start-chat.dart index ec53d0fa5..c706044de 100644 --- a/lib/views/widgets/dialogs/dialog-start-chat.dart +++ b/lib/views/widgets/dialogs/dialog-start-chat.dart @@ -1,12 +1,9 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/libs/matrix/auth.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/store/auth/actions.dart'; diff --git a/lib/views/widgets/dialogs/dialog-text-input.dart b/lib/views/widgets/dialogs/dialog-text-input.dart index 966d84997..355120fbd 100644 --- a/lib/views/widgets/dialogs/dialog-text-input.dart +++ b/lib/views/widgets/dialogs/dialog-text-input.dart @@ -1,11 +1,10 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; +import 'package:syphon/views/widgets/loader/loading-indicator.dart'; class DialogTextInput extends StatefulWidget { const DialogTextInput({ @@ -147,19 +146,7 @@ class _DialogTextInputState extends State { }, child: !widget.loading ? Text(Strings.buttonSaveGeneric) - : Container( - constraints: BoxConstraints( - maxHeight: 16, - maxWidth: 16, - ), - child: CircularProgressIndicator( - strokeWidth: Dimensions.defaultStrokeWidth, - backgroundColor: Colors.white, - valueColor: AlwaysStoppedAnimation( - Colors.grey, - ), - ), - ), + : LoadingIndicator(size: 16), ), ], ) diff --git a/lib/views/widgets/image-matrix.dart b/lib/views/widgets/image-matrix.dart index 96b19897b..93e8444b5 100644 --- a/lib/views/widgets/image-matrix.dart +++ b/lib/views/widgets/image-matrix.dart @@ -1,17 +1,13 @@ -// Dart imports: import 'dart:typed_data'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/index.dart'; import 'package:syphon/store/media/actions.dart'; @@ -83,13 +79,12 @@ class MatrixImageState extends State { // Created in attempts to reduce framerate drop in chat details // not sure this actually works as it still drops on scroll - if (this.disableRebuild && mediaCache.containsKey(widget.mxcUri)) { + if (disableRebuild && mediaCache.containsKey(widget.mxcUri)) { finalUriData = mediaCache[widget.mxcUri!]; } } MatrixImageState({ - Key? key, this.forceLoading = false, this.disableRebuild = false, }); @@ -122,7 +117,7 @@ class MatrixImageState extends State { height: widget.size ?? widget.height, child: CircularProgressIndicator( strokeWidth: widget.strokeWidth * 1.5, - valueColor: new AlwaysStoppedAnimation( + valueColor: AlwaysStoppedAnimation( Theme.of(context).accentColor, ), value: null, diff --git a/lib/views/widgets/input/text-field-secure.dart b/lib/views/widgets/input/text-field-secure.dart index 7fff5fb53..2833b7211 100644 --- a/lib/views/widgets/input/text-field-secure.dart +++ b/lib/views/widgets/input/text-field-secure.dart @@ -1,22 +1,20 @@ -// Flutter imports: import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; -/** - * Secured Text Field Input - * - * Remove all auto completions by default - * Other functionality that could indicate - * text content - */ +/// +/// Secured Text Field Input +/// +/// Remove all auto completions by default +/// Other functionality that could indicate +/// text content +/// class TextFieldSecure extends StatelessWidget { - TextFieldSecure({ + const TextFieldSecure({ Key? key, this.label, this.hint, @@ -28,6 +26,8 @@ class TextFieldSecure extends StatelessWidget { this.disabled = false, this.obscureText = false, this.disableSpacing = false, + this.autocorrect = false, + this.enabledSuggestions = false, this.textAlign = TextAlign.left, this.formatters = const [], this.onChanged, @@ -41,6 +41,9 @@ class TextFieldSecure extends StatelessWidget { final bool disabled; final bool obscureText; final bool disableSpacing; + final bool autocorrect; + final bool enabledSuggestions; + final int maxLines; final Widget? suffix; // include actions final String? hint; @@ -73,19 +76,19 @@ class TextFieldSecure extends StatelessWidget { onSubmitted: onSubmitted as void Function(String)?, textInputAction: textInputAction, onEditingComplete: onEditingComplete as void Function()?, - autocorrect: false, - enableSuggestions: false, + autocorrect: autocorrect, + enableSuggestions: enabledSuggestions, autofillHints: autofillHints, selectionHeightStyle: BoxHeightStyle.max, inputFormatters: !disableSpacing ? [ - FilteringTextInputFormatter.deny(RegExp(r"\t")), + FilteringTextInputFormatter.deny(RegExp(r'\t')), ...formatters, ] : [ - FilteringTextInputFormatter.deny(RegExp(r"\s")), - FilteringTextInputFormatter.deny(RegExp(r"\t")), - FilteringTextInputFormatter.deny(RegExp(r"\n")), + FilteringTextInputFormatter.deny(RegExp(r'\s')), + FilteringTextInputFormatter.deny(RegExp(r'\t')), + FilteringTextInputFormatter.deny(RegExp(r'\n')), ...formatters, ], smartQuotesType: SmartQuotesType.disabled, diff --git a/lib/views/widgets/lists/list-user-bubbles.dart b/lib/views/widgets/lists/list-user-bubbles.dart index 009519757..0ec3d1d13 100644 --- a/lib/views/widgets/lists/list-user-bubbles.dart +++ b/lib/views/widgets/lists/list-user-bubbles.dart @@ -1,25 +1,22 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/user/model.dart'; import 'package:syphon/store/user/selectors.dart'; -import 'package:syphon/views/home/chat/details-all-users-screen.dart'; +import 'package:syphon/views/home/chat/chat-detail-all-users-screen.dart'; import 'package:syphon/views/home/groups/invite-users-screen.dart'; import 'package:syphon/views/widgets/avatars/avatar.dart'; import 'package:syphon/views/widgets/modals/modal-user-details.dart'; -/** - * List of Users (Avi Bubbles) - * - * Still uses userId because users - * are still indexed by room - */ +/// +/// List of Users (Avi Bubbles) +/// +/// Still uses userId because users +/// are still indexed by room class ListUserBubbles extends StatelessWidget { - ListUserBubbles({ + const ListUserBubbles({ Key? key, this.users = const [], this.roomId = '', @@ -34,7 +31,6 @@ class ListUserBubbles extends StatelessWidget { final String? roomId; final List users; - @protected onShowUserDetails({ required BuildContext context, User? user, @@ -79,7 +75,7 @@ class ListUserBubbles extends StatelessWidget { uri: user.avatarUri, alt: user.displayName ?? user.userId, size: Dimensions.avatarSize, - background: Colours.hashedColor(formatUsername(user)), + background: Colours.hashedColor(user.userId), ), ), ), @@ -93,8 +89,7 @@ class ListUserBubbles extends StatelessWidget { padding: EdgeInsets.symmetric(vertical: 14), child: ClipOval( child: Material( - color: - Theme.of(context).scaffoldBackgroundColor, // button color + color: Theme.of(context).scaffoldBackgroundColor, // button color child: InkWell( onTap: () { if (invite) { @@ -129,9 +124,7 @@ class ListUserBubbles extends StatelessWidget { ), child: Icon( invite ? Icons.edit : Icons.arrow_forward_ios, - size: invite - ? Dimensions.iconSize - : Dimensions.iconSizeLarge, + size: invite ? Dimensions.iconSize : Dimensions.iconSizeLarge, color: Theme.of(context).textTheme.caption!.color, ), ), diff --git a/lib/views/widgets/loader/index.dart b/lib/views/widgets/loader/index.dart index 74103847b..9f8f79cec 100644 --- a/lib/views/widgets/loader/index.dart +++ b/lib/views/widgets/loader/index.dart @@ -1,12 +1,10 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; class Loader extends StatelessWidget { - Loader({ + const Loader({ Key? key, this.loading = false, }) : super(key: key); @@ -15,7 +13,7 @@ class Loader extends StatelessWidget { @override Widget build(BuildContext context) => Visibility( - visible: this.loading, + visible: loading, child: Container( margin: EdgeInsets.only(top: 8), child: Row( diff --git a/lib/views/widgets/loader/loading-indicator.dart b/lib/views/widgets/loader/loading-indicator.dart new file mode 100644 index 000000000..617d854a6 --- /dev/null +++ b/lib/views/widgets/loader/loading-indicator.dart @@ -0,0 +1,30 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:syphon/global/dimensions.dart'; + +class LoadingIndicator extends StatelessWidget { + const LoadingIndicator({ + Key? key, + this.size = 28, + this.loading = false, + }) : super(key: key); + + final double size; + final bool loading; + + @override + Widget build(BuildContext context) => Container( + constraints: BoxConstraints( + maxWidth: size, + maxHeight: size, + ), + child: CircularProgressIndicator( + strokeWidth: Dimensions.defaultStrokeWidth, + backgroundColor: Colors.white, + valueColor: AlwaysStoppedAnimation( + Colors.grey, + ), + ), + ); +} diff --git a/lib/views/widgets/messages/message-typing.dart b/lib/views/widgets/messages/message-typing.dart index 72675721d..d32a53acf 100644 --- a/lib/views/widgets/messages/message-typing.dart +++ b/lib/views/widgets/messages/message-typing.dart @@ -1,11 +1,9 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:syphon/global/colours.dart'; import 'package:syphon/global/print.dart'; -// Project imports: import 'package:syphon/store/user/model.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/views/widgets/avatars/avatar.dart'; diff --git a/lib/views/widgets/messages/message.dart b/lib/views/widgets/messages/message.dart index e9ade518f..3c0a08526 100644 --- a/lib/views/widgets/messages/message.dart +++ b/lib/views/widgets/messages/message.dart @@ -1,10 +1,8 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:swipeable/swipeable.dart'; -// Project imports: import 'package:syphon/global/colours.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/formatters.dart'; @@ -16,10 +14,10 @@ import 'package:syphon/views/widgets/avatars/avatar.dart'; import 'package:syphon/views/widgets/messages/styles.dart'; class MessageWidget extends StatelessWidget { - MessageWidget({ + const MessageWidget({ Key? key, required this.message, - this.isUserSent, + this.isUserSent = false, this.messageOnly = false, this.isLastSender = false, this.isNextSender = false, @@ -29,6 +27,7 @@ class MessageWidget extends StatelessWidget { this.theme = ThemeType.LIGHT, this.fontSize = 14.0, this.timeFormat = '12hr', + this.color, this.onLongPress, this.onPressAvatar, this.onInputReaction, @@ -38,15 +37,17 @@ class MessageWidget extends StatelessWidget { final Message message; final ThemeType theme; - final int? lastRead; + final int lastRead; final double fontSize; final bool isLastSender; final bool isNextSender; - final bool? isUserSent; + final bool isUserSent; final bool messageOnly; final String timeFormat; final String? avatarUri; final String? selectedMessageId; + final Color? color; + final Function? onSwipe; final Function? onPressAvatar; final Function? onInputReaction; @@ -90,7 +91,7 @@ class MessageWidget extends StatelessWidget { width: reactionCount > 1 ? 48 : 32, height: 48, decoration: BoxDecoration( - color: Colors.grey[500], + color: Color(Colours.greyDefault), borderRadius: BorderRadius.circular(Dimensions.iconSize), border: Border.all( color: Colors.white, @@ -149,7 +150,7 @@ class MessageWidget extends StatelessWidget { width: 36, height: Dimensions.iconSizeLarge, decoration: BoxDecoration( - color: Colors.grey[500], + color: Color(Colours.greyDefault), borderRadius: BorderRadius.circular(Dimensions.iconSizeLarge), border: Border.all( color: Colors.white, @@ -181,21 +182,26 @@ class MessageWidget extends StatelessWidget { ); } + onSwipeMessage(Message message) { + if (onSwipe != null) { + onSwipe!(message); + } + } + @override Widget build(BuildContext context) { final message = this.message; - final selected = - selectedMessageId != null && selectedMessageId == message.id; + final selected = selectedMessageId != null && selectedMessageId == message.id; // emoji input button needs space final hasReactions = message.reactions.isNotEmpty || selected; - final isRead = message.timestamp! < lastRead!; + final isRead = message.timestamp < lastRead; var textColor = Colors.white; var showSender = true; var indicatorColor = Theme.of(context).iconTheme.color; var indicatorIconColor = Theme.of(context).iconTheme.color; - Color? bubbleColor = Colours.hashedColor(message.sender); + Color? bubbleColor = color ?? Colours.hashedColor(message.sender); var bubbleBorder = BorderRadius.circular(16); var alignmentMessage = MainAxisAlignment.start; var alignmentReaction = MainAxisAlignment.start; @@ -206,18 +212,18 @@ class MessageWidget extends StatelessWidget { var zIndex = 1.0; var status = timeFormat == 'full' ? formatTimestampFull( - lastUpdateMillis: message.timestamp ?? 0, + lastUpdateMillis: message.timestamp, timeFormat: timeFormat, showTime: true, ) : formatTimestamp( - lastUpdateMillis: message.timestamp ?? 0, + lastUpdateMillis: message.timestamp, timeFormat: timeFormat, showTime: true, ); // Current User Bubble Styling - if (isUserSent!) { + if (isUserSent) { if (isLastSender) { if (isNextSender) { // Message in the middle of a sender messages block @@ -257,14 +263,14 @@ class MessageWidget extends StatelessWidget { } } - if (isUserSent!) { + if (isUserSent) { if (theme == ThemeType.DARK) { - bubbleColor = Colors.grey[700]; + bubbleColor = Color(Colours.greyDark); } else if (theme != ThemeType.LIGHT) { - bubbleColor = Colors.grey[850]; + bubbleColor = Color(Colours.greyDarkest); } else { textColor = const Color(Colours.blackFull); - bubbleColor = const Color(Colours.greyBubble); + bubbleColor = const Color(Colours.greyLightest); } indicatorColor = isRead ? textColor : bubbleColor; @@ -305,19 +311,19 @@ class MessageWidget extends StatelessWidget { } return Swipeable( - onSwipeLeft: isUserSent! ? () => onSwipe!(message) : null, - onSwipeRight: !isUserSent! ? () => onSwipe!(message) : null, + onSwipeLeft: isUserSent ? () => onSwipeMessage(message) : () => {}, + onSwipeRight: !isUserSent ? () => onSwipeMessage(message) : () => {}, background: Positioned( top: 0, bottom: 0, - left: !isUserSent! ? 0 : null, - right: isUserSent! ? 0 : null, + left: !isUserSent ? 0 : null, + right: isUserSent ? 0 : null, child: Opacity( // HACK: hide the reply icon under the message opacity: opacity == 0.5 ? 0 : 1, child: Container( padding: EdgeInsets.symmetric( - horizontal: isUserSent! ? 24 : 50, + horizontal: isUserSent ? 24 : 50, ), child: Flex( direction: Axis.horizontal, @@ -348,8 +354,7 @@ class MessageWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - margin: - bubbleSpacing, // spacing between different user bubbles + margin: bubbleSpacing, // spacing between different user bubbles padding: const EdgeInsets.symmetric(horizontal: 12), child: Flex( direction: Axis.horizontal, @@ -357,7 +362,7 @@ class MessageWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Visibility( - visible: !isLastSender && !isUserSent! && !messageOnly, + visible: !isLastSender && !isUserSent && !messageOnly, maintainState: !messageOnly, maintainAnimation: !messageOnly, maintainSize: !messageOnly, @@ -369,8 +374,7 @@ class MessageWidget extends StatelessWidget { } }, child: Container( - margin: const EdgeInsets.only(right: 8) - .copyWith(bottom: hasReactions ? 16 : 0), + margin: const EdgeInsets.only(right: 8).copyWith(bottom: hasReactions ? 16 : 0), child: Avatar( margin: EdgeInsets.zero, padding: EdgeInsets.zero, @@ -406,7 +410,7 @@ class MessageWidget extends StatelessWidget { crossAxisAlignment: alignmentMessageText, children: [ Visibility( - visible: !isUserSent! && showSender, + visible: !isUserSent && showSender, child: Container( margin: EdgeInsets.only(bottom: 4), child: Text( @@ -427,10 +431,7 @@ class MessageWidget extends StatelessWidget { color: textColor, fontStyle: fontStyle, fontWeight: FontWeight.w300, - fontSize: Theme.of(context) - .textTheme - .subtitle2! - .fontSize, + fontSize: Theme.of(context).textTheme.subtitle2!.fontSize, ), ), ), @@ -442,9 +443,7 @@ class MessageWidget extends StatelessWidget { crossAxisAlignment: alignmentMessageText, children: [ Visibility( - visible: !isUserSent! && - message.type == - EventTypes.encrypted, + visible: !isUserSent && message.type == EventTypes.encrypted, child: Container( width: Dimensions.indicatorSize, height: Dimensions.indicatorSize, @@ -469,9 +468,7 @@ class MessageWidget extends StatelessWidget { ), ), Visibility( - visible: isUserSent! && - message.type == - EventTypes.encrypted, + visible: isUserSent && message.type == EventTypes.encrypted, child: Container( width: Dimensions.indicatorSize, height: Dimensions.indicatorSize, @@ -484,7 +481,7 @@ class MessageWidget extends StatelessWidget { ), ), Visibility( - visible: isUserSent! && message.failed, + visible: isUserSent && message.failed, child: Container( width: Dimensions.indicatorSize, height: Dimensions.indicatorSize, @@ -497,7 +494,7 @@ class MessageWidget extends StatelessWidget { ), ), Visibility( - visible: isUserSent! && !message.failed, + visible: isUserSent && !message.failed, child: Stack(children: [ Visibility( visible: message.pending, @@ -506,8 +503,7 @@ class MessageWidget extends StatelessWidget { height: Dimensions.indicatorSize, margin: EdgeInsets.only(left: 4), child: CircularProgressIndicator( - strokeWidth: Dimensions - .defaultStrokeWidthLite, + strokeWidth: Dimensions.defaultStrokeWidthLite, ), ), ), @@ -565,17 +561,16 @@ class MessageWidget extends StatelessWidget { Visibility( visible: selected, child: Positioned( - left: isUserSent! ? 0 : null, - right: !isUserSent! ? 0 : null, + left: isUserSent ? 0 : null, + right: !isUserSent ? 0 : null, bottom: 0, child: Container( height: Dimensions.iconSize, - transform: - Matrix4.translationValues(0.0, 4.0, 0.0), + transform: Matrix4.translationValues(0.0, 4.0, 0.0), child: buildReactionsInput( context, alignmentReaction, - isUserSent!, + isUserSent, ), ), ), @@ -583,13 +578,12 @@ class MessageWidget extends StatelessWidget { Visibility( visible: hasReactions && !selected, child: Positioned( - left: isUserSent! ? 0 : null, - right: !isUserSent! ? 0 : null, + left: isUserSent ? 0 : null, + right: !isUserSent ? 0 : null, bottom: 0, child: Container( height: Dimensions.iconSize, - transform: - Matrix4.translationValues(0.0, 4.0, 0.0), + transform: Matrix4.translationValues(0.0, 4.0, 0.0), child: buildReactions( context, alignmentReaction, diff --git a/lib/views/widgets/modals/modal-image-options.dart b/lib/views/widgets/modals/modal-image-options.dart index 399266523..81bd7241c 100644 --- a/lib/views/widgets/modals/modal-image-options.dart +++ b/lib/views/widgets/modals/modal-image-options.dart @@ -1,15 +1,11 @@ -// Dart imports: import 'dart:async'; import 'dart:io'; -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:image_picker/image_picker.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; class ModalImageOptions extends StatelessWidget { diff --git a/lib/views/widgets/modals/modal-user-details.dart b/lib/views/widgets/modals/modal-user-details.dart index ed61e99fe..ff8c8ed67 100644 --- a/lib/views/widgets/modals/modal-user-details.dart +++ b/lib/views/widgets/modals/modal-user-details.dart @@ -1,8 +1,6 @@ -// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// Package imports: import 'package:equatable/equatable.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/svg.dart'; @@ -10,7 +8,6 @@ import 'package:redux/redux.dart'; import 'package:syphon/global/assets.dart'; import 'package:syphon/global/colours.dart'; -// Project imports: import 'package:syphon/global/dimensions.dart'; import 'package:syphon/global/strings.dart'; import 'package:syphon/store/index.dart'; @@ -35,9 +32,7 @@ class ModalUserDetails extends StatelessWidget { final String? userId; final bool? nested; // pop context twice when double nested in a view - @protected - void onNavigateToProfile( - {required BuildContext context, required _Props props}) async { + onNavigateToProfile({required BuildContext context, required _Props props}) async { Navigator.pushNamed( context, '/home/user/details', @@ -47,9 +42,7 @@ class ModalUserDetails extends StatelessWidget { ); } - @protected - void onNavigateToInvite( - {required BuildContext context, required _Props props}) async { + onNavigateToInvite({required BuildContext context, required _Props props}) async { Navigator.pushNamed( context, '/home/rooms/search', @@ -59,9 +52,7 @@ class ModalUserDetails extends StatelessWidget { ); } - @protected - void onMessageUser( - {required BuildContext context, required _Props props}) async { + onMessageUser({required BuildContext context, required _Props props}) async { final user = props.user; return await showDialog( context: context, @@ -138,9 +129,8 @@ class ModalUserDetails extends StatelessWidget { uri: props.user.avatarUri, alt: props.user.displayName ?? props.user.userId, size: Dimensions.avatarSizeDetails, - background: props.user.avatarUri == null - ? Colours.hashedColor(props.user.userId) - : null, + background: + props.user.avatarUri == null ? Colours.hashedColor(props.user.userId) : null, ), ), ], @@ -153,10 +143,9 @@ class ModalUserDetails extends StatelessWidget { props.user.displayName ?? '', overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, - style: - Theme.of(context).textTheme.subtitle1!.copyWith( - fontWeight: FontWeight.w500, - ), + style: Theme.of(context).textTheme.subtitle1!.copyWith( + fontWeight: FontWeight.w500, + ), ), ) ], diff --git a/pubspec.lock b/pubspec.lock index 0b16881bc..e441b5cde 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -116,12 +116,10 @@ packages: canonical_json: dependency: "direct main" description: - path: "." - ref: main - resolved-ref: dab7d28ded523a4e0532c3f7c653587de6997b8a - url: "https://github.com/ereio/flutter_canonical_json" - source: git - version: "2.0.0" + name: canonical_json + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" characters: dependency: transitive description: @@ -185,13 +183,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" - crypt: - dependency: "direct main" - description: - name: crypt - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.1" crypto: dependency: "direct main" description: @@ -909,8 +900,8 @@ packages: dependency: "direct main" description: path: "." - ref: null-safety - resolved-ref: "46a6372a463db5dda757e608d9a7939e5e92f50e" + ref: "867b117f15f51404e31597d8ab9d5325f6b52938" + resolved-ref: "867b117f15f51404e31597d8ab9d5325f6b52938" url: "https://github.com/ereio/swipeable.git" source: git version: "2.0.0" @@ -929,7 +920,7 @@ packages: source: hosted version: "1.2.0" test: - dependency: transitive + dependency: "direct dev" description: name: test url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 18dfdecd7..8cf61e89f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,7 @@ description: a privacy focused matrix client # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. -version: 0.1.8+181 +version: 0.1.9+190 environment: sdk: ">=2.12.0 <3.0.0" # <- modified to solve build_runner @@ -63,12 +63,7 @@ dependencies: swipeable: git: url: https://github.com/ereio/swipeable.git - ref: null-safety - - canonical_json: - git: - url: https://github.com/ereio/flutter_canonical_json - ref: main + ref: 867b117f15f51404e31597d8ab9d5325f6b52938 # state equatable: ^2.0.0 @@ -86,9 +81,9 @@ dependencies: # encryption olm: ^2.0.0 - crypt: ^4.0.1 crypto: ^3.0.1 encrypt: ^5.0.0 + canonical_json: 1.1.0 # cache/storage sembast: ^3.0.1 @@ -135,7 +130,6 @@ dependencies: collection: ^1.15.0-nullsafety.4 # UNSOUND NULL-SAFTEY - # canonical_json: 1.0.0 # flutter_material_color_picker: ^1.0.5 # swipeable: 1.1.0 @@ -144,6 +138,7 @@ dev_dependencies: build_runner: ^2.0.2 json_serializable: ^4.1.0 flutter_launcher_icons: ^0.9.0 + test: any flutter_icons: android: true diff --git a/version.txt b/version.txt index d9cfdae6b..c80292fdb 100644 --- a/version.txt +++ b/version.txt @@ -1,2 +1,2 @@ -versionName=0.1.6+3 -versionCode=163 +versionName=0.1.9 +versionCode=190