diff --git a/android/app/src/main/kotlin/com/twake/twake/Application.kt b/android/app/src/main/kotlin/com/twake/twake/Application.kt index c0b46883..e3c4c7ab 100644 --- a/android/app/src/main/kotlin/com/twake/twake/Application.kt +++ b/android/app/src/main/kotlin/com/twake/twake/Application.kt @@ -4,16 +4,17 @@ import io.flutter.app.FlutterApplication import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback import io.flutter.plugins.GeneratedPluginRegistrant -import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService +/* import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService */ +import io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingBackgroundService; class Application : FlutterApplication(), PluginRegistrantCallback { override fun onCreate() { super.onCreate() - FlutterFirebaseMessagingService.setPluginRegistrant(this); + FlutterFirebaseMessagingBackgroundService.setPluginRegistrant(this); } override fun registerWith(registry: PluginRegistry?) { - io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin.registerWith(registry?.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin")); + /* GeneratedPluginRegistrant.registerWith(registry); */ } } diff --git a/android/build.gradle b/android/build.gradle index bd773afa..dc83390a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // Add the google services classpath classpath 'com.google.gms:google-services:4.3.2' diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 296b146b..493072b3 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/ios/Podfile b/ios/Podfile index 1ce6b058..1e8c3c90 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,7 +1,6 @@ # Uncomment this line to define a global platform for your project # platform :ios, '9.0' - # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 978e644d..1227219d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -192,6 +192,6 @@ SPEC CHECKSUMS: url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96 -PODFILE CHECKSUM: 955a4ce6ca109c044ef3069e126096ea43d23878 +PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c COCOAPODS: 1.10.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index de4f2855..973a6c68 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -361,7 +361,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 1; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 6Z27TKCGWF; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -380,8 +382,10 @@ MARKETING_VERSION = 2.2.2; PRODUCT_BUNDLE_IDENTIFIER = twake; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -504,7 +508,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 1; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 6Z27TKCGWF; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -523,9 +529,11 @@ MARKETING_VERSION = 2.2.2; PRODUCT_BUNDLE_IDENTIFIER = twake; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -537,7 +545,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 1; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 6Z27TKCGWF; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -556,8 +566,10 @@ MARKETING_VERSION = 2.2.2; PRODUCT_BUNDLE_IDENTIFIER = twake; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87c278db..46d2d0d8 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - PreviewsEnabled - - - diff --git a/lib/blocs/auth_bloc/auth_bloc.dart b/lib/blocs/auth_bloc/auth_bloc.dart index eb1388ab..e6376743 100644 --- a/lib/blocs/auth_bloc/auth_bloc.dart +++ b/lib/blocs/auth_bloc/auth_bloc.dart @@ -44,7 +44,7 @@ class AuthBloc extends Bloc { // print('CONSOLE LINK: ${repository.twakeConsole}'); if (repository.authMode == 'INTERNAL') return; this.webView = HeadlessInAppWebView( - initialUrl: repository.twakeConsole, + initialUrlRequest: URLRequest(url: Uri.parse(repository.twakeConsole)), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( cacheEnabled: false, @@ -54,18 +54,22 @@ class AuthBloc extends Bloc { // onConsoleMessage: (ctrl, msg) => print('CONSOLEJS: $msg'), onLoadStop: (ctrl, url) async { // print('URL: $url'); - if (Uri.parse(_prevUrl).path == Uri.parse(url).path) { + if (Uri.parse(_prevUrl).path == url.path) { this.add(WrongAuthCredentials()); _prevUrl = ''; return; } - _prevUrl = url; - if (url.contains('redirect_to_app')) { - final qp = Uri.parse(url).queryParameters; + _prevUrl = url.path; + if (url.path.contains('redirect_to_app')) { + final qp = url.queryParameters; // Logger().d('PARAMS: $qp'); if (qp['token'] == null || qp['username'] == null) { repository.logger.e('NO TOKEN AND USERNAME'); - ctrl.loadUrl(url: repository.twakeConsole); + ctrl.loadUrl( + urlRequest: URLRequest( + url: Uri.parse(repository.twakeConsole), + ), + ); this.add(WrongAuthCredentials()); return; } @@ -102,7 +106,6 @@ class AuthBloc extends Bloc { yield Authenticated(initData); break; case AuthResult.NetworkError: - // TODO Work out the case with absent network connection final InitData initData = await initMain(); yield Authenticated(initData); break; @@ -212,7 +215,6 @@ class AuthBloc extends Bloc { return; } await CookieManager.instance().deleteAllCookies(); - await CookieManager.instance().getCookies(url: 'auth.twake.app'); _prevUrl = ''; await webView.dispose(); // print('Running webview...'); diff --git a/lib/blocs/directs_bloc/directs_bloc.dart b/lib/blocs/directs_bloc/directs_bloc.dart index 044f57e8..ce6a6177 100644 --- a/lib/blocs/directs_bloc/directs_bloc.dart +++ b/lib/blocs/directs_bloc/directs_bloc.dart @@ -67,6 +67,8 @@ class DirectsBloc extends BaseChannelBloc { @override Stream mapEventToState(ChannelsEvent event) async* { + print('Event in DirectsBloc: $event'); + if (event is ReloadChannels) { yield ChannelsLoading(); final filter = { diff --git a/lib/blocs/fields_cubit/fields_cubit.dart b/lib/blocs/fields_cubit/fields_cubit.dart index 2181bd7a..5ee6832d 100644 --- a/lib/blocs/fields_cubit/fields_cubit.dart +++ b/lib/blocs/fields_cubit/fields_cubit.dart @@ -1,3 +1,4 @@ +import 'package:meta/meta.dart'; import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; import 'package:twake/repositories/fields_repository.dart'; @@ -8,25 +9,28 @@ class FieldsCubit extends Cubit { FieldsCubit(this.repository) : super(FieldsInitial()); - void add(Widget field, int index) async { - final result = await repository.add(field, index); + Future add({@required Widget field, @required int atIndex}) async { + final result = await repository.add(field, atIndex); // print('Current map: ${repository.data}'); emit(Added(fields: result)); } - void remove(int index) async { - final result = await repository.remove(index); + Future remove({@required int atIndex}) async { + final result = await repository.remove(atIndex); // print('Current map: ${repository.data}'); emit(Removed(fields: result)); } - void update(int index, String content) async { - final result = await repository.updateData(index, content); + Future update({ + @required String withContent, + @required int atIndex, + }) async { + final result = await repository.updateData(atIndex, withContent); // print('Current map: $result'); emit(Updated(data: result)); } - void clear() async { + Future clear() async { final result = await repository.clear(); emit( result.isEmpty diff --git a/lib/blocs/fields_cubit/fields_state.dart b/lib/blocs/fields_cubit/fields_state.dart index 0511cd7d..7321560d 100644 --- a/lib/blocs/fields_cubit/fields_state.dart +++ b/lib/blocs/fields_cubit/fields_state.dart @@ -47,7 +47,7 @@ class Updated extends FieldsState { Updated({@required this.data}); @override - List get props => [fields]; + List get props => [data]; @override List get fields => []; @@ -59,7 +59,7 @@ class Error extends FieldsState { Error(this.message); @override - List get props => []; + List get props => [message]; @override List get fields => []; diff --git a/lib/blocs/notification_bloc/notification_bloc.dart b/lib/blocs/notification_bloc/notification_bloc.dart index 449172d9..66d08e41 100644 --- a/lib/blocs/notification_bloc/notification_bloc.dart +++ b/lib/blocs/notification_bloc/notification_bloc.dart @@ -34,7 +34,7 @@ class NotificationBloc extends Bloc { final logger = Logger(); final _api = Api(); - final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); + final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; Map subscriptionRooms = {}; StreamSubscription _subscription; @@ -58,8 +58,9 @@ class NotificationBloc extends Bloc { authBloc.repository.socketIOHost, IO.OptionBuilder() .setPath('/socket') - .setTimeout(10000) + .enableAutoConnect() .disableAutoConnect() + .enableReconnection() .setTransports(['websocket']).build(), ); _authSubscription = authBloc.listen((state) { @@ -79,17 +80,20 @@ class NotificationBloc extends Bloc { }); setupListeners(); if (connectionBloc.state is ConnectionActive) { - socket = socket.connect(); + if (socket.disconnected) socket.connect(); } } - void _iOSpermission() { - _firebaseMessaging.requestNotificationPermissions( - IosNotificationSettings(sound: true, badge: true, alert: true)); - _firebaseMessaging.onIosSettingsRegistered - .listen((IosNotificationSettings settings) { - print("Settings registered: $settings"); - }); + void _iOSpermission() async { + await _firebaseMessaging.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, + ); } void setupListeners() { @@ -103,12 +107,12 @@ class NotificationBloc extends Bloc { socketConnectionState = SocketConnectionState.CONNECTED; while (socketConnectionState != SocketConnectionState.AUTHENTICATED && authBloc.repository.accessToken != null) { + print('AUTHENTICATING SOCKEIO'); if (socket.disconnected) socket = socket.connect(); socket.emit(SocketIOEvent.AUTHENTICATE, { 'token': authBloc.repository.accessToken, }); await Future.delayed(Duration(seconds: 5)); - print('WAITING FOR SOCKET AUTH'); } }); socket.onError((e) => logger.e('ERROR ON SOCKET \n$e')); @@ -125,15 +129,15 @@ class NotificationBloc extends Bloc { // logger.d('PING $ping'); // }); socket.on(SocketIOEvent.EVENT, (data) { - logger.d('GOT EVENT: $data'); + // logger.d('GOT EVENT: $data'); handleSocketEvent(data); }); socket.on(SocketIOEvent.RESOURCE, (data) { - logger.d('GOT RESOURCE: $data'); + // logger.d('GOT RESOURCE: $data'); handleSocketResource(data); }); socket.on(SocketIOEvent.JOIN_ERROR, (data) { - logger.d('FAILED TO JOIN: $data'); + logger.d('FAILED TO JOIN TO SOCKEIO ROOM: $data'); }); socket.on(SocketIOEvent.JOIN_SUCCESS, (data) { // logger.d('SUCCESSFUL JOIN: $data'); @@ -141,8 +145,15 @@ class NotificationBloc extends Bloc { } void reinit() async { - if (connectionBloc.state is ConnectionLost) return; - if (socket.disconnected) socket = socket.connect(); + while (true) { + if (connectionBloc.state is ConnectionLost) return; + if (socket.disconnected) socket = socket.connect(); + // Wait for the socket to authenticate; + await Future.delayed(Duration(seconds: 3)); + if (this.socketConnectionState == SocketConnectionState.AUTHENTICATED) { + break; + } + } for (String room in subscriptionRooms.keys) { unsubscribe(room); } diff --git a/lib/main.dart b/lib/main.dart index 9aba5039..6da7219b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:connectivity/connectivity.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -17,6 +18,7 @@ import 'package:twake/utils/sentry.dart'; void main() async { runZonedGuarded>(() async { WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); final AuthRepository authRepository = await initAuth(); final ConfigurationRepository configurationRepository = diff --git a/lib/pages/edit_channel.dart b/lib/pages/edit_channel.dart index a7dfa7b8..247150a9 100644 --- a/lib/pages/edit_channel.dart +++ b/lib/pages/edit_channel.dart @@ -74,7 +74,9 @@ class _EditChannelState extends State { _nameController.addListener(() { final channelName = _nameController.text; _batchUpdateState(name: channelName); - if (channelName.isNotReallyEmpty && !_canSave && _channelName != channelName) { + if (channelName.isNotReallyEmpty && + !_canSave && + _channelName != channelName) { setState(() { _channelName = channelName; _canSave = true; @@ -203,7 +205,7 @@ class _EditChannelState extends State { _panelController.open(); } - void _toggleEmojiBoard() async { + Future _toggleEmojiBoard() async { FocusScope.of(context).requestFocus(FocusNode()); await Future.delayed(Duration(milliseconds: 150)); setState(() { @@ -213,11 +215,11 @@ class _EditChannelState extends State { Widget _buildEmojiBoard() { return EmojiKeyboard( - onEmojiSelected: (emoji) { + onEmojiSelected: (emoji) async { _canSave = true; _icon = emoji.text; _batchUpdateState(icon: _icon); - _toggleEmojiBoard(); + await _toggleEmojiBoard(); }, height: MediaQuery.of(context).size.height * 0.35, ); @@ -267,169 +269,168 @@ class _EditChannelState extends State { ), body: SafeArea( bottom: false, - child: BlocBuilder( - buildWhen: (_, current) => + child: BlocListener( + listenWhen: (_, current) => current is EditChannelSaved || current is EditChannelDeleted, - builder: (context, state) { - // print('EditChannel State: $state'); + listener: (context, state) { + print('EditChannel State: $state'); if (state is EditChannelSaved || state is EditChannelDeleted) { context .read() .add(ReloadChannels(forceFromApi: true)); Navigator.of(context).pop([state]); } - return GestureDetector( - onTap: () => _closeKeyboards(context), - behavior: HitTestBehavior.opaque, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.fromLTRB(16.0, 17.0, 16.0, 20.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - if (_emojiVisible) { - setState(() { - _emojiVisible = false; - }); - } else { - Navigator.of(context).pop(); - } - }, - child: Text( - 'Cancel', - style: TextStyle( - color: Color(0xff3840f7), - fontSize: 17.0, - fontWeight: FontWeight.w500, - ), + }, + child: GestureDetector( + onTap: () => _closeKeyboards(context), + behavior: HitTestBehavior.opaque, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(16.0, 17.0, 16.0, 20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + if (_emojiVisible) { + setState(() { + _emojiVisible = false; + }); + } else { + Navigator.of(context).pop(); + } + }, + child: Text( + 'Cancel', + style: TextStyle( + color: Color(0xff3840f7), + fontSize: 17.0, + fontWeight: FontWeight.w500, ), ), - Column( - children: [ - SelectableAvatar( - size: 74.0, - icon: _icon, - onTap: () => _toggleEmojiBoard(), - ), - SizedBox(height: 4.0), - GestureDetector( - onTap: () => _toggleEmojiBoard(), - child: Text('Change avatar', - style: TextStyle( - color: Color(0xff3840f7), - fontSize: 13.0, - fontWeight: FontWeight.w400, - )), - ), - ], - ), - GestureDetector( - onTap: _canSave ? () => _save() : null, - child: Text( - 'Save', - style: TextStyle( - color: _canSave - ? Color(0xff3840f7) - : Color(0xffa2a2a2), - fontSize: 17.0, - fontWeight: FontWeight.w500, - ), + ), + Column( + children: [ + SelectableAvatar( + size: 74.0, + icon: _icon, + onTap: () => _toggleEmojiBoard(), ), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - RoundedBoxButton( - cover: - Image.asset('assets/images/add_new_member.png'), - title: 'add', - onTap: () => _openAdd(context), + SizedBox(height: 4.0), + GestureDetector( + onTap: () => _toggleEmojiBoard(), + child: Text('Change avatar', + style: TextStyle( + color: Color(0xff3840f7), + fontSize: 13.0, + fontWeight: FontWeight.w400, + )), + ), + ], ), - SizedBox(width: 10.0), - RoundedBoxButton( - cover: Image.asset('assets/images/delete.png'), - title: 'delete', - color: Color(0xfff04820), - onTap: () => _delete(context), + GestureDetector( + onTap: _canSave ? () => _save() : null, + child: Text( + 'Save', + style: TextStyle( + color: _canSave + ? Color(0xff3840f7) + : Color(0xffa2a2a2), + fontSize: 17.0, + fontWeight: FontWeight.w500, + ), + ), ), ], ), - SizedBox(height: 24.0), - HintLine(text: 'CHANNEL INFORMATION', isLarge: true), - SizedBox(height: 12.0), - Divider( - thickness: 0.5, - height: 0.5, - color: Colors.black.withOpacity(0.2), - ), - SheetTextField( - hint: 'Channel name', - controller: _nameController, - focusNode: _nameFocusNode, - maxLength: 30, - ), - Divider( - thickness: 0.5, - height: 0.5, - color: Colors.black.withOpacity(0.2), - ), - SheetTextField( - hint: 'Description', - controller: _descriptionController, - focusNode: _descriptionFocusNode, - ), - Divider( - thickness: 0.5, - height: 0.5, - color: Colors.black.withOpacity(0.2), - ), - // ButtonField( - // title: 'Channel type', - // trailingTitle: 'Public', - // hasArrow: true, - // ), - SizedBox(height: 32.0), - HintLine(text: 'MEMBERS', isLarge: true), - SizedBox(height: 12.0), - Divider( - thickness: 0.5, - height: 0.5, - color: Colors.black.withOpacity(0.2), - ), - ButtonField( - title: 'Member management', - trailingTitle: 'Manage', - hasArrow: true, - onTap: () => _openManagement(context), - ), - Divider( - thickness: 0.5, - height: 0.5, - color: Colors.black.withOpacity(0.2), - ), - // SwitchField( - // title: 'Chat history for new members', - // value: _showHistoryForNew, - // onChanged: (value) => - // _batchUpdateState(showHistoryForNew: value), - // isExtended: true, - // ), - // SizedBox(height: 8.0), - // HintLine(text: 'Show previous chat history for newly added members'), - Spacer(), - _emojiVisible ? _buildEmojiBoard() : Container(), - ], - ), - ); - }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RoundedBoxButton( + cover: Image.asset('assets/images/add_new_member.png'), + title: 'add', + onTap: () => _openAdd(context), + ), + SizedBox(width: 10.0), + RoundedBoxButton( + cover: Image.asset('assets/images/delete.png'), + title: 'delete', + color: Color(0xfff04820), + onTap: () => _delete(context), + ), + ], + ), + SizedBox(height: 24.0), + HintLine(text: 'CHANNEL INFORMATION', isLarge: true), + SizedBox(height: 12.0), + Divider( + thickness: 0.5, + height: 0.5, + color: Colors.black.withOpacity(0.2), + ), + SheetTextField( + hint: 'Channel name', + controller: _nameController, + focusNode: _nameFocusNode, + maxLength: 30, + ), + Divider( + thickness: 0.5, + height: 0.5, + color: Colors.black.withOpacity(0.2), + ), + SheetTextField( + hint: 'Description', + controller: _descriptionController, + focusNode: _descriptionFocusNode, + ), + Divider( + thickness: 0.5, + height: 0.5, + color: Colors.black.withOpacity(0.2), + ), + // ButtonField( + // title: 'Channel type', + // trailingTitle: 'Public', + // hasArrow: true, + // ), + SizedBox(height: 32.0), + HintLine(text: 'MEMBERS', isLarge: true), + SizedBox(height: 12.0), + Divider( + thickness: 0.5, + height: 0.5, + color: Colors.black.withOpacity(0.2), + ), + ButtonField( + title: 'Member management', + trailingTitle: 'Manage', + hasArrow: true, + onTap: () => _openManagement(context), + ), + Divider( + thickness: 0.5, + height: 0.5, + color: Colors.black.withOpacity(0.2), + ), + // SwitchField( + // title: 'Chat history for new members', + // value: _showHistoryForNew, + // onChanged: (value) => + // _batchUpdateState(showHistoryForNew: value), + // isExtended: true, + // ), + // SizedBox(height: 8.0), + // HintLine(text: 'Show previous chat history for newly added members'), + Spacer(), + _emojiVisible ? _buildEmojiBoard() : Container(), + ], + ), + ), ), ), ), @@ -440,9 +441,11 @@ class _EditChannelState extends State { if (both) { FocusScope.of(context).requestFocus(FocusNode()); } - setState(() { - _emojiVisible = false; - }); + if (_emojiVisible) { + setState(() { + _emojiVisible = false; + }); + } } } diff --git a/lib/pages/messages_page.dart b/lib/pages/messages_page.dart index 9f9b2d95..6fabb22b 100644 --- a/lib/pages/messages_page.dart +++ b/lib/pages/messages_page.dart @@ -16,6 +16,7 @@ import 'package:twake/repositories/draft_repository.dart'; import 'package:twake/widgets/common/stacked_image_avatars.dart'; import 'package:twake/widgets/common/text_avatar.dart'; import 'package:twake/widgets/common/shimmer_loading.dart'; +import 'package:twake/widgets/common/channel_title.dart'; import 'package:twake/widgets/message/message_edit_field.dart'; import 'package:twake/widgets/message/messages_grouped_list.dart'; import 'package:twake/utils/navigation.dart'; @@ -128,32 +129,17 @@ class MessagesPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: ShimmerLoading( - key: ValueKey('name'), - isLoading: parentChannel.name == null, - width: 60.0, - height: 10.0, - child: Text( - parentChannel.name ?? '', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w600, - color: Color(0xff444444), - ), - overflow: TextOverflow.ellipsis, - ), - ), - ), - SizedBox(width: 6), - if ((parentChannel is Channel) && + ShimmerLoading( + key: ValueKey('name'), + isLoading: parentChannel.name == null, + width: 60.0, + height: 10.0, + child: ChannelTitle( + name: parentChannel.name ?? '', + isPrivate: (parentChannel is Channel) && parentChannel.visibility != null && - parentChannel.visibility == 'private') - Icon(Icons.lock_outline, - size: 15.0, color: Color(0xff444444)), - ], + parentChannel.visibility == 'private', + ), ), SizedBox(height: 4), if (parentChannel is Channel) @@ -229,33 +215,36 @@ class MessagesPage extends StatelessWidget { } return BlocBuilder( - builder: (ctx, state) => MessageEditField( - autofocus: state is MessageEditing, - initialText: - state is MessageEditing ? state.originalStr : draft, - onMessageSend: state is MessageEditing - ? state.onMessageEditComplete - : (content) { - BlocProvider.of>(context).add( - SendMessage(content: content), - ); - context.read().add( - ResetDraft( - id: channelId, type: draftType), - ); - }, - onTextUpdated: state is MessageEditing - ? (text) {} - : (text) { - context.read().add( - UpdateDraft( - id: channelId, - type: draftType, - draft: text, - ), - ); - }, - ), + builder: (ctx, state) { + return MessageEditField( + autofocus: state is MessageEditing, + initialText: state is MessageEditing + ? state.originalStr + : draft, + onMessageSend: state is MessageEditing + ? state.onMessageEditComplete + : (content) { + BlocProvider.of>(context).add( + SendMessage(content: content), + ); + context.read().add( + ResetDraft( + id: channelId, type: draftType), + ); + }, + onTextUpdated: state is MessageEditing + ? (text) {} + : (text) { + context.read().add( + UpdateDraft( + id: channelId, + type: draftType, + draft: text, + ), + ); + }, + ); + }, ); }, ), @@ -269,10 +258,10 @@ class MessagesPage extends StatelessWidget { void _goEdit(BuildContext context, MessagesState state) async { final params = await openEditChannel(context, state.parentChannel); - if (params.length != 0) { + if (params != null && params.length > 0) { final editingState = params.first; if (editingState is EditChannelDeleted) { - Navigator.of(context).pop(); + Navigator.of(context).maybePop(); } } } diff --git a/lib/repositories/auth_repository.dart b/lib/repositories/auth_repository.dart index f613d07f..128a0785 100644 --- a/lib/repositories/auth_repository.dart +++ b/lib/repositories/auth_repository.dart @@ -68,7 +68,7 @@ class AuthRepository extends JsonSerializable { // print('Actual host for auth: ${configurationRepository.host}'); Api.host = configurationRepository.host; - final fcmToken = (await FirebaseMessaging().getToken()); + final fcmToken = (await FirebaseMessaging.instance.getToken()); // print('FCM TOKEN: $fcmToken'); final apiVersion = (await PackageInfo.fromPlatform()).version; diff --git a/lib/repositories/fields_repository.dart b/lib/repositories/fields_repository.dart index a093af8e..5b3e7abe 100644 --- a/lib/repositories/fields_repository.dart +++ b/lib/repositories/fields_repository.dart @@ -51,7 +51,12 @@ class FieldsRepository { } Future> updateData(int index, String content) async { + print('Content: $content'); + print('Index: $index'); + data[index] = content; + print('Data: $data'); + return Map.from(data); } } diff --git a/lib/repositories/profile_repository.dart b/lib/repositories/profile_repository.dart index dac15308..b9c38fc0 100644 --- a/lib/repositories/profile_repository.dart +++ b/lib/repositories/profile_repository.dart @@ -58,15 +58,16 @@ class ProfileRepository extends JsonSerializable { // Pseudo constructor for loading profile from storage or api static Future load() async { + logger.w("Loading profile"); bool loadedFromNetwork = false; var profileMap = await _storage.load( type: StorageType.Profile, key: _PROFILE_STORE_KEY, ); if (profileMap == null) { - // logger.d('No profile in storage, requesting from api...'); + logger.d('No profile in storage, requesting from api...'); profileMap = await _api.get(Endpoint.profile); - // logger.d('RECEIVED PROFILE: $profileMap'); + logger.d('RECEIVED PROFILE: $profileMap'); loadedFromNetwork = true; } else { profileMap = jsonDecode(profileMap[_storage.settingsField]); diff --git a/lib/services/api.dart b/lib/services/api.dart index d2508efa..b0ff04f8 100644 --- a/lib/services/api.dart +++ b/lib/services/api.dart @@ -130,17 +130,13 @@ class Api { queryParameters: params, ); try { - final s = Stopwatch(); - s.start(); - final response = await (useTokenDio ? tokenDio : dio).getUri(uri); - s.stop(); - // logger.d('GET HEADERS: ${dio.options.headers}'); + // logger.d('METHOD: $url'); // logger.d('PARAMS: $params'); - // logger.d( - // 'METHOD: ${uri.toString()}\nTOOK: ${s.elapsedMilliseconds / 1000} seconds'); + final response = await (useTokenDio ? tokenDio : dio).getUri(uri); // logger.d('GET RESPONSE: ${response.data}'); return response.data; } catch (e) { + logger.wtf('FAILED TO GET INFO: $e'); throw ApiError.fromDioError(e); } } @@ -228,30 +224,29 @@ class Api { dio.interceptors.add( InterceptorsWrapper( // token validation causes infinite loop - onRequest: (options) async { + onRequest: (options, handler) async { if (!(await autoProlongToken())) { - return dio.reject('Both tokens have expired'); + return handler.reject(DioError( + error: 'Both tokens have expired', requestOptions: options)); } - // print('REQUEST HEADERS: ${options.headers}\n' - // 'DIO HEADERS: ${dio.options.headers}'); options.headers['Authorization'] = dio.options.headers['Authorization']; + handler.next(options); }, - onError: (DioError error) async { + onError: (DioError error, ErrorInterceptorHandler handler) async { // Due to the bugs in JWT handling from twake api side, // we randomly get token expirations, so if we have a // refresh token, we automatically use it to get a new token if (error.response != null) { logger.e('Error during network request!' + - '\nMethod: ${error.request.method}' + - '\nPATH: ${error.request.path}' + - '\nHeaders: ${error.request.headers}' + + '\nMethod: ${error.requestOptions.method}' + + '\nPATH: ${error.requestOptions.path}' + + '\nHeaders: ${error.requestOptions.headers}' + '\nResponse: ${error.response.data}' + - '\nBODY: ${jsonEncode(error.request.data)}' + - '\nQUERY: ${error.request.queryParameters}'); + '\nBODY: ${jsonEncode(error.requestOptions.data)}' + + '\nQUERY: ${error.requestOptions.queryParameters}'); } else { logger.wtf("UNEXPECTED NETWORK ERROR:\n$error"); - return error; } if (error.response.statusCode == 401 && _prolongToken != null) { logger.e('Token has expired prematuraly, prolonging...'); @@ -261,8 +256,8 @@ class Api { _invalidateConfiguration(); } else { logger.e('status code: ${error.response.statusCode}'); - return error; } + handler.next(error); }, ), ); @@ -288,7 +283,7 @@ class ApiError implements Exception { factory ApiError.fromDioError(DioError error) { var apiErrorType = ApiErrorType.Unknown; if (error.response == null) { - Logger().wtf("UNEXPECTED ERROR:\n$error"); + Logger().wtf("UNEXPECTED ERROR:\n${error.error}"); apiErrorType = ApiErrorType.Unauthorized; } else if (error.response.statusCode == 500) { apiErrorType = ApiErrorType.ServerError; diff --git a/lib/services/init.dart b/lib/services/init.dart index 0a65ee82..1a2d6a09 100644 --- a/lib/services/init.dart +++ b/lib/services/init.dart @@ -42,92 +42,96 @@ Future initAuth() async { } Future initMain() async { - // await Emojis.load(); - final profile = await ProfileRepository.load(); - await profile.syncBadges(); - final sheet = await SheetRepository.load(); - final addChannel = await AddChannelRepository.load(); - final addDirect = AddDirectRepository(); - final editChannel = await EditChannelRepository.load(); - final addWorkspace = AddWorkspaceRepository(); - final fields = FieldsRepository(fields: [], data: {}); - final draft = DraftRepository(); - final _ = UserRepository(Endpoint.users); - final companies = await CollectionRepository.load( - Endpoint.companies, - sortFields: {'name': true}, - ); - final workspaces = await CollectionRepository.load( - Endpoint.workspaces, - filters: [ - ['company_id', '=', companies.selected.id] - ], - queryParams: {'company_id': companies.selected.id}, - sortFields: {'name': true}, - ); - final channels = await CollectionRepository.load( - Endpoint.channels, - queryParams: { - 'workspace_id': workspaces.selected.id, - 'company_id': companies.selected.id, - }, - filters: [ - ['workspace_id', '=', workspaces.selected.id] - ], - sortFields: {'name': true}, - ); - final directs = await CollectionRepository.load( - Endpoint.directs, - queryParams: { - 'company_id': companies.selected.id, - }, - sortFields: {'last_activity': false}, - filters: [ - ['company_id', '=', companies.selected.id] - ], - ); - // final directs = - // CollectionRepository(items: [], apiEndpoint: Endpoint.directs); - final messages = - MessagesRepository(items: [], apiEndpoint: Endpoint.messages); - final messagesDirect = - MessagesRepository(items: [], apiEndpoint: Endpoint.messages); - final threads = MessagesRepository(items: [], apiEndpoint: Endpoint.messages); - var channelMembers; - if (!channels.isEmpty) { - channelMembers = await MemberRepository.load( - Endpoint.channelMembers, + print("INIT MAIN"); + try { + final profile = await ProfileRepository.load(); + await profile.syncBadges(); + final sheet = await SheetRepository.load(); + final addChannel = await AddChannelRepository.load(); + final addDirect = AddDirectRepository(); + final editChannel = await EditChannelRepository.load(); + final addWorkspace = AddWorkspaceRepository(); + final fields = FieldsRepository(fields: [], data: {}); + final draft = DraftRepository(); + final _ = UserRepository(Endpoint.users); + final companies = await CollectionRepository.load( + Endpoint.companies, + sortFields: {'name': true}, + ); + final workspaces = await CollectionRepository.load( + Endpoint.workspaces, + filters: [ + ['company_id', '=', companies.selected.id] + ], + queryParams: {'company_id': companies.selected.id}, + sortFields: {'name': true}, + ); + final channels = await CollectionRepository.load( + Endpoint.channels, queryParams: { - 'company_id': companies.selected.id, 'workspace_id': workspaces.selected.id, - 'channel_id': channels.selected.id, + 'company_id': companies.selected.id, }, - sortFields: {'channel_id': true}, + filters: [ + ['workspace_id', '=', workspaces.selected.id] + ], + sortFields: {'name': true}, ); - } else - channelMembers = MemberRepository( - items: [], - apiEndpoint: Endpoint.channelMembers, + final directs = await CollectionRepository.load( + Endpoint.directs, + queryParams: { + 'company_id': companies.selected.id, + }, + sortFields: {'last_activity': false}, + filters: [ + ['company_id', '=', companies.selected.id] + ], ); - - return InitData( - profile: profile, - companies: companies, - workspaces: workspaces, - channels: channels, - directs: directs, - messages: messages, - messagesDirect: messagesDirect, - threads: threads, - sheet: sheet, - addChannel: addChannel, - addDirect: addDirect, - editChannel: editChannel, - channelMembers: channelMembers, - addWorkspace: addWorkspace, - draft: draft, - fields: fields, - ); + final messages = + MessagesRepository(items: [], apiEndpoint: Endpoint.messages); + final messagesDirect = + MessagesRepository(items: [], apiEndpoint: Endpoint.messages); + final threads = + MessagesRepository(items: [], apiEndpoint: Endpoint.messages); + var channelMembers; + if (!channels.isEmpty) { + channelMembers = await MemberRepository.load( + Endpoint.channelMembers, + queryParams: { + 'company_id': companies.selected.id, + 'workspace_id': workspaces.selected.id, + 'channel_id': channels.selected.id, + }, + sortFields: {'channel_id': true}, + ); + } else + channelMembers = MemberRepository( + items: [], + apiEndpoint: Endpoint.channelMembers, + ); + return InitData( + profile: profile, + companies: companies, + workspaces: workspaces, + channels: channels, + directs: directs, + messages: messages, + messagesDirect: messagesDirect, + threads: threads, + sheet: sheet, + addChannel: addChannel, + addDirect: addDirect, + editChannel: editChannel, + channelMembers: channelMembers, + addWorkspace: addWorkspace, + draft: draft, + fields: fields, + ); + } catch (e) { + Logger().wtf("WHOA, ERROR: {}", e); + } + print("FINISHED INITIALIZING MAIN"); + return InitData(); } class InitData { diff --git a/lib/services/notifications.dart b/lib/services/notifications.dart index 6557e6f7..d757b147 100644 --- a/lib/services/notifications.dart +++ b/lib/services/notifications.dart @@ -2,8 +2,6 @@ import 'dart:convert'; import 'dart:io' show Platform; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:twake/models/notification.dart'; import 'package:twake/services/service_bundle.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; @@ -15,65 +13,15 @@ class Notifications { final Function(MessageNotification) onResumeCallback; final Function(MessageNotification) onLaunchCallback; final bool Function(MessageNotification) shouldNotify; - FirebaseMessaging _fcm = FirebaseMessaging(); FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); Map> pendingNotifications = {}; var counter = 0; - // final _api = Api(); -// Future onDidReceiveLocalNotification( - // int id, String title, String body, String payload) async { - // // display a dialog with the notification details, tap ok to go to another page - // showDialog( - // context: context, - // builder: (BuildContext context) => CupertinoAlertDialog( - // title: Text(title), - // content: Text(body), - // actions: [ - // CupertinoDialogAction( - // isDefaultAction: true, - // child: Text('Ok'), - // onPressed: () async { - // Navigator.of(context, rootNavigator: true).pop(); - // await Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => SecondScreen(payload), - // ), - // ); - // }, - // ) - // ], - // ), - // ); -// } Future onDidReceiveLocalNotification( int id, String title, String body, String payload) async { // display a dialog with the notification details, tap ok to go to another page print('SHOW IOS NOTIFICATION'); - // showDialog( - // context: context, - // builder: (BuildContext context) => CupertinoAlertDialog( - // title: Text(title), - // content: Text(body), - // actions: [ - // CupertinoDialogAction( - // isDefaultAction: true, - // child: Text('Ok'), - // onPressed: () async { - // Navigator.of(context, rootNavigator: true).pop(); - // await Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => SecondScreen(payload), - // ), - // ); - // }, - // ) - // ], - // ), - // ); } Notifications({ @@ -91,18 +39,12 @@ class Notifications { else if (Platform.isMacOS) this.platform = Target.MacOS; else if (Platform.isWindows) this.platform = Target.Windows; - _fcm.configure( - onMessage: onMessage, - onResume: onResume, - onLaunch: onLaunch, - ); + FirebaseMessaging.onMessage.listen(onMessage); const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('logo_blue'); - // final IOSInitializationSettings initializationSettingsIOS = - // IOSInitializationSettings(onDidReceiveLocalNotification: onDidReceiveLocalNotification); final IOSInitializationSettings initializationSettingsIOS = - IOSInitializationSettings(); + IOSInitializationSettings(); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, @@ -124,21 +66,9 @@ class Notifications { }); } - // Future checkWhatsNew(String workspaceId) async { - // final List news = await _api.get( - // Endpoint.whatsNew, - // params: { - // 'company_id': ProfileBloc.selectedCompany, - // 'workspace_id': workspaceId - // }, - // ); - // for (Map item in news) { - // final update = WhatsNewItem.fromJson(item); - // } - // } - - Future onMessage(Map message) async { - // logger.d('GOT MESSAGE FROM FIREBASE: $message'); + Future onMessage(RemoteMessage rmessage) async { + Map message = rmessage.data; + logger.d('GOT MESSAGE FROM FIREBASE: $message, ${rmessage.toString()}'); final notification = messageParse(message); if (!shouldNotify(notification)) return; const AndroidNotificationDetails androidPlatformChannelSpecifics = @@ -151,10 +81,6 @@ class Notifications { NotificationDetails(android: androidPlatformChannelSpecifics); final channelId = _getChannelId(message); - // logger.d('channelId: $channelId'); - // logger.d('title: ${_getTitle(message)}'); - // logger.d('body: ${_getBody(message)}'); - // logger.d('payload: ${_getPayload(message)}'); if (pendingNotifications[channelId] == null) { pendingNotifications[channelId] = []; @@ -163,8 +89,8 @@ class Notifications { await flutterLocalNotificationsPlugin.show( counter, - _getTitle(message), - _getBody(message), + rmessage.notification.title, + rmessage.notification.body, platformChannelSpecifics, payload: _getPayload(message), ); @@ -195,50 +121,50 @@ class Notifications { return notification; } - String _getBody(Map message) { - var data; - switch (platform) { - case Target.Android: - // logger.d('Android notification received\n$message'); - data = message['notification']['body']; - break; - case Target.IOS: - // logger.d('iOS notification received\n$message'); - data = message['aps']['alert']['body']; - break; - case Target.Linux: - case Target.MacOS: - case Target.Windows: - throw 'Desktop is not supported'; - } - return data; - } - - String _getTitle(Map message) { - var data; - switch (platform) { - case Target.Android: - // logger.d('Android notification received\n$message'); - data = message['notification']['title']; - break; - case Target.IOS: - // logger.d('iOS notification received\n$message'); - data = message['aps']['alert']['title']; - break; - case Target.Linux: - case Target.MacOS: - case Target.Windows: - throw 'Desktop is not supported'; - } - return data; - } + // String _getBody(Map message) { + // var data; + // switch (platform) { + // case Target.Android: + // logger.d('Android notification received\n$message'); + // data = message['body']; + // break; + // case Target.IOS: + // logger.d('iOS notification received\n$message'); + // data = message['aps']['alert']['body']; + // break; + // case Target.Linux: + // case Target.MacOS: + // case Target.Windows: + // throw 'Desktop is not supported'; + // } + // return data; + // } +// + // String _getTitle(Map message) { + // var data; + // switch (platform) { + // case Target.Android: + // logger.d('Android notification received\n$message'); + // data = message['title']; + // break; + // case Target.IOS: + // logger.d('iOS notification received\n$message'); + // data = message['aps']['alert']['title']; + // break; + // case Target.Linux: + // case Target.MacOS: + // case Target.Windows: + // throw 'Desktop is not supported'; + // } + // return data; + // } String _getPayload(Map message) { var data; switch (platform) { case Target.Android: // logger.d('Android notification received\n$message'); - data = message['data']['notification_data']; + data = message['notification_data']; break; case Target.IOS: // logger.d('iOS notification received\n$message'); @@ -260,7 +186,7 @@ class Notifications { switch (platform) { case Target.Android: // logger.d('Android notification received\n$message'); - data = message['data']['notification_data']; + data = message['notification_data']; break; case Target.IOS: // logger.d('iOS notification received\n$message'); @@ -277,16 +203,16 @@ class Notifications { return data['channel_id']; } - Future onResume(Map message) async { - logger.d('Resuming on message received\n$message'); - final notification = messageParse(message); - // logger.d("ok, that's what we have:\n$notification"); - await onResumeCallback(notification); - } + // Future onResume(RemoteMessage rmessage) async { + // Map message = rmessage.data; + // logger.d('Resuming on message received\n$message'); + // final notification = messageParse(message); + // await onResumeCallback(notification); + // } - Future onLaunch(Map message) async { - onResume(message); - } + // Future onLaunch(RemoteMessage message) async { + // onResume(message); + // } } enum Target { diff --git a/lib/utils/twacode.dart b/lib/utils/twacode.dart new file mode 100644 index 00000000..fb6b15d1 --- /dev/null +++ b/lib/utils/twacode.dart @@ -0,0 +1,504 @@ +import 'package:tuple/tuple.dart'; + +class TwacodeParser { + final String original; + static final RegExp userMatch = RegExp('([a-zA-z0-9_]+):([a-zA-z0-9-]+)'); + + List nodes = []; + + TwacodeParser(this.original) { + parse(); + } + + List get message => nodes.map((n) => n.transform()).toList(); + + void parse() { + int start = 0; + for (int i = 0; i < original.length - 1; i++) { + // Bold text + if (original[i] == Delim.star && original[i + 1] == Delim.star) { + final index = this.doesCloseBold(i + 2); + if (index != 0) { + this.nodes.add( + ASTNode(type: TType.Text, text: original.substring(start, i)), + ); + this.nodes.add(ASTNode( + type: TType.Bold, text: original.substring(i + 2, index - 2))); + i = start = index; + } + } + // Underline text + else if (original[i] == Delim.underline && + original[i + 1] == Delim.underline) { + final index = doesCloseUnderline(i + 2); + if (index != 0) { + this.nodes.add( + ASTNode(type: TType.Text, text: original.substring(start, i)), + ); + this.nodes.add(ASTNode( + type: TType.Underline, + text: original.substring(i + 2, index - 2), + )); + i = start = index; + } + } + // Italic text + else if (original[i] == Delim.underline && + original[i + 1] != Delim.underline) { + final index = doesCloseItalic(i + 1); + if (index != 0) { + this.nodes.add( + ASTNode(type: TType.Text, text: original.substring(start, i)), + ); + this.nodes.add(ASTNode( + type: TType.Italic, + text: original.substring(i + 1, index - 1), + )); + i = start = index; + } + } + // StrikeThrough text + else if (original[i] == Delim.tilde && original[i + 1] == Delim.tilde) { + final index = doesCloseStrikeThrough(i + 2); + if (index != 0) { + this.nodes.add( + ASTNode(type: TType.Text, text: original.substring(start, i)), + ); + this.nodes.add(ASTNode( + type: TType.StrikeThrough, + text: original.substring(i + 2, index - 2), + )); + i = start = index; + } + } + // Newline text + else if (original[i] == Delim.lf) { + this.nodes.add( + ASTNode(type: TType.Text, text: original.substring(start, i)), + ); + this.nodes.add(ASTNode( + type: TType.LineBreak, + text: "", + )); + start = i + 1; + } + // Newline detection + else if (original[i] == Delim.gt) { + if (nodes.isEmpty || nodes.last.type == TType.LineBreak) { + int index = this.hasLineFeed(i + 1); + index = index != 0 ? index : original.length + 1; + this.nodes.add(ASTNode( + type: TType.Quote, + text: original.substring(i + 1, index - 1), + )); + i = start = index; + } + } + // Username + else if (original[i] == Delim.at && + (i == 0 || + original[i - 1] == Delim.ws || + original[i - 1] == Delim.lf)) { + final index = this.isUser(i + 1); + if (index != 0) { + this.nodes.add( + ASTNode(type: TType.Text, text: original.substring(start, i)), + ); + this.nodes.add(ASTNode( + type: TType.User, + text: original.substring(i + 1, index), + )); + i = start = index; + } + } + // Email + else if (original[i] == Delim.at && + (i != 0 && + original[i - 1] != Delim.ws && + original[i - 1] != Delim.lf)) { + final range = isEmail(i); + if (range.item1 != 0 || range.item2 != 0) { + this.nodes.add( + ASTNode( + type: TType.Text, + text: original.substring(start, range.item1), + ), + ); + this.nodes.add(ASTNode( + type: TType.Email, + text: original.substring(range.item1, range.item2), + )); + i = start = range.item2; + } + } + // URL with full protocol description like https://hello.world + else if (original[i] == Delim.slash && original[i + 1] == Delim.slash) { + final range = this.isUrl(i + 1); + if (range.item1 != 0 || range.item2 != 0) { + this.nodes.add( + ASTNode( + type: TType.Text, + text: original.substring(start, range.item1), + ), + ); + this.nodes.add(ASTNode( + type: TType.Url, + text: original.substring(range.item1, range.item2), + )); + i = start = range.item2; + } + } + // InlineCode text + else if (original[i] == Delim.tick && original[i + 1] != Delim.tick) { + final index = this.doesCloseInlineCode(i + 1); + if (index != 0) { + this.nodes.add( + ASTNode(type: TType.Text, text: original.substring(start, i)), + ); + this.nodes.add(ASTNode( + type: TType.InlineCode, + text: original.substring(i + 1, index - 1), + )); + i = start = index; + } + } + // MultiLineCode text + else if (original[i] == Delim.tick && + original[i + 1] == Delim.tick && + i + 2 < original.length && + original[i + 2] == Delim.tick) { + final index = this.doesCloseMultiCode(i + 3); + if (index != 0) { + final acc = original.substring(start, i); + if (acc.isNotEmpty) { + this.nodes.add( + ASTNode( + type: TType.Text, + text: original.substring(start, i), + ), + ); + } + this.nodes.add(ASTNode( + type: TType.MultiLineCode, + text: original.substring(i + 3, index - 3), + )); + i = start = index; + } + } + // #Channel + else if (original[i] == Delim.pound && + original[i + 1] != Delim.ws && + original[i + 1] != Delim.lf) { + final index = this.isChannel(i + 1); + final acc = original.substring(start, i); + if (acc.isNotEmpty) { + this.nodes.add( + ASTNode( + type: TType.Text, + text: original.substring(start, i), + ), + ); + } + this.nodes.add(ASTNode( + type: TType.Channel, + text: original.substring(i + 1, index), + )); + i = start = index; + } + } + if (start < original.length) { + this.nodes.add(ASTNode( + type: TType.Text, + text: original.substring(start), + )); + } + if (this.nodes.first.text.isEmpty) { + this.nodes.removeAt(0); + } + if (this.nodes.last.text.isEmpty) { + this.nodes.removeLast(); + } + } + + int doesCloseBold(int i) { + final len = original.length - 1; + for (int j = i; j < len && original[j] != '\n'; j++) { + if (original[j] == Delim.star && original[j + 1] == Delim.star) { + return j + 2; + } + } + return 0; + } + + int doesCloseItalic(int i) { + final len = original.length; + for (int j = i; j < len && original[j] != '\n'; j++) { + if (original[j] == Delim.underline) { + return j + 1; + } + } + return 0; + } + + Tuple2 isEmail(int i) { + var start = i; + var end = i; + while (start > 0) { + final cur = original[start - 1]; + if (cur == Delim.ws || cur == Delim.lf) { + break; + } + start -= 1; + } + while (end < original.length) { + final cur = original[end]; + if (cur == Delim.ws || cur == Delim.lf) { + break; + } + end += 1; + } + final parts = original.substring(start, end).split('@'); + if (parts[0].isEmpty || parts[1].isEmpty) { + return Tuple2(0, 0); + } + final subparts = parts[1].split('.'); + final p1 = parts[0].codeUnits.every((c) => c > 32 && c < 127) && + parts[0].codeUnits.every((c) => c > 32 && c < 127); + final p2 = + subparts.length > 1 && subparts[0].isNotEmpty && subparts[1].isNotEmpty; + if (p1 && p2) { + return Tuple2(start, end); + } + + return Tuple2(0, 0); + } + + Tuple2 isUrl(int i) { + var start = i; + var end = i; + while (start > 0) { + final cur = original[start - 1]; + if (cur == Delim.ws || cur == Delim.lf) { + break; + } + start -= 1; + } + while (end < original.length) { + final cur = original[end]; + if (cur == Delim.ws || cur == Delim.lf) { + break; + } + end += 1; + } + final parts = original.substring(start, end).split('://'); + if (parts[0].isEmpty || parts[1].isEmpty) { + return Tuple2(0, 0); + } + final p1 = parts[0] == 'http' || parts[0] == 'https'; + // pretty dumb check, buut, for now it will do + final p2 = parts[1].contains('.'); + if (p1 && p2) { + return Tuple2(start, end); + } + + return Tuple2(0, 0); + } + + int doesCloseUnderline(int i) { + final len = original.length - 1; + for (int j = i; j < len && original[j] != '\n'; j++) { + if (original[j] == Delim.underline && + original[j + 1] == Delim.underline) { + return j + 2; + } + } + return 0; + } + + int isUser(int i) { + for (int j = i; j < original.length; j++) { + if (original[j] == Delim.ws || original[j] == Delim.lf) { + if (userMatch.hasMatch(original.substring(i, j))) { + return j; + } else { + return 0; + } + } + } + if (userMatch.hasMatch(original.substring(i))) { + return original.length; + } else { + return 0; + } + } + + int isChannel(int i) { + for (int j = i; j < original.length; j++) { + if (original[j] == Delim.ws || original[j] == Delim.lf) { + return j; + } + } + return original.length; + } + + int doesCloseStrikeThrough(int i) { + final len = original.length - 1; + for (int j = i; j < len && original[j] != '\n'; j++) { + if (original[j] == Delim.tilde && original[j + 1] == Delim.tilde) { + return j + 2; + } + } + return 0; + } + + int doesCloseInlineCode(int i) { + final len = original.length; + for (int j = i; j < len && original[j] != '\n'; j++) { + if (original[j] == Delim.tick) { + return j + 1; + } + } + return 0; + } + + int doesCloseMultiCode(int i) { + final len = original.length; + int ticks = 0; + for (int j = i; j < len; j++) { + if (original[j] == Delim.tick) { + if (ticks == 2) { + return j + 1; + } else { + ticks += 1; + } + } else { + ticks = 0; + } + } + return 0; + } + + int hasLineFeed(int i) { + final len = original.length; + for (int j = i; j < len; j++) { + if (original[j] == Delim.lf) { + return j + 1; + } + } + return 0; + } +} + +class ASTNode { + TType type; + String text; + ASTNode({this.type, this.text}); + + dynamic transform() { + Map map = {}; + switch (this.type) { + case TType.Text: + return this.text; + + case TType.LineBreak: + map['start'] = ''; + map['end'] = '\n'; + map['content'] = []; + break; + + case TType.InlineCode: + map['start'] = '`'; + map['end'] = '`'; + map['content'] = this.text; + break; + + case TType.MultiLineCode: + map['start'] = '```'; + map['end'] = '```'; + map['content'] = this.text; + break; + + case TType.Underline: + map['start'] = '__'; + map['end'] = '__'; + map['content'] = this.text; + break; + + case TType.StrikeThrough: + map['start'] = '~~'; + map['end'] = '~~'; + map['content'] = this.text; + break; + + case TType.Bold: + map['start'] = '**'; + map['end'] = '**'; + map['content'] = this.text; + break; + + case TType.Italic: + map['start'] = '_'; + map['end'] = '_'; + map['content'] = this.text; + break; + + case TType.Quote: + map['start'] = '>'; + map['content'] = this.text; + break; + + case TType.User: + map['start'] = '@'; + map['content'] = this.text; + break; + + case TType.Channel: + map['start'] = '#'; + map['content'] = this.text; + break; + + case TType.Url: + map['type'] = 'url'; + map['content'] = this.text; + break; + + case TType.Email: + map['type'] = 'email'; + map['content'] = this.text; + break; + + default: + throw Exception('Unsupported twacode type'); + } + return map; + } +} + +enum TType { + Text, + LineBreak, + InlineCode, + MultiLineCode, + Underline, + StrikeThrough, + Bold, + Italic, + Quote, + User, + Channel, + Url, + Email +} + +class Delim { + static String star = '*'; + static String underline = '_'; + static String tilde = '~'; + static String gt = '>'; + static String tick = '`'; + static String slash = '/'; + static String at = '@'; + static String pound = '#'; + static String lf = '\n'; + static String ws = ' '; +} diff --git a/lib/widgets/channel/channel_tile.dart b/lib/widgets/channel/channel_tile.dart index 2fbff7e7..b8c7649e 100644 --- a/lib/widgets/channel/channel_tile.dart +++ b/lib/widgets/channel/channel_tile.dart @@ -9,6 +9,7 @@ import 'package:twake/repositories/draft_repository.dart'; import 'package:twake/utils/dateformatter.dart'; import 'package:twake/utils/navigation.dart'; import 'package:twake/widgets/common/text_avatar.dart'; +import 'package:twake/widgets/common/channel_title.dart'; class ChannelTile extends StatelessWidget { final Channel channel; @@ -40,83 +41,71 @@ class ChannelTile extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( + child: ChannelTitle( + name: channel.name, + hasUnread: channel.hasUnread == 1, + isPrivate: channel.visibility != null && + channel.visibility == 'private', + ), + ), + Text( + DateFormatter.getVerboseDateTime(channel.lastActivity), + style: Theme.of(context).textTheme.subtitle2, + ), + ], + ), + Row( + children: [ + Padding( + padding: EdgeInsets.only(top: 4.0), child: Text( - channel.name, + channel.lastMessage['text'] ?? 'No messages yet', overflow: TextOverflow.ellipsis, textAlign: TextAlign.start, style: TextStyle( - fontSize: 16.0, - fontWeight: channel.hasUnread == 1 - ? FontWeight.w900 - : FontWeight.w400, + fontSize: 12.0, + fontWeight: FontWeight.w400, color: Color(0xff444444), ), ), ), - SizedBox(width: 6), - if (channel.visibility != null && - channel.visibility == 'private') - Icon(Icons.lock_outline, - size: 17.0, color: Color(0xff444444)), - ], - ), - Padding( - padding: EdgeInsets.only(top: 4.0), - child: Text( - channel.lastMessage['text'] ?? 'No messages yet', - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, - style: TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.w400, - color: Color(0xff444444), + Spacer(), + if (channel.messagesUnread != 0) SizedBox(width: Dim.wm2), + // if (channel.messagesUnread != 0) + BlocBuilder( + buildWhen: (_, curr) => curr is ProfileLoaded, + builder: (ctx, state) { + final count = (state as ProfileLoaded) + .getBadgeForChannel(channel.id); + if (count > 0) { + return Badge( + shape: BadgeShape.square, + borderRadius: + BorderRadius.all(Radius.circular(5)), + padding: EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + badgeContent: Text( + '$count', + style: TextStyle( + color: Colors.white, + fontSize: Dim.tm2(), + ), + ), + ); + } else { + return Container(); + } + }, ), - ), + ], ), ], ), ), - SizedBox(width: 15), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - DateFormatter.getVerboseDateTime(channel.lastActivity), - style: Theme.of(context).textTheme.subtitle2, - ), - if (channel.messagesUnread != 0) SizedBox(width: Dim.wm2), - // if (channel.messagesUnread != 0) - BlocBuilder( - buildWhen: (prev, curr) => curr is ProfileLoaded, - builder: (ctx, state) { - final count = - (state as ProfileLoaded).getBadgeForChannel(channel.id); - if (count > 0) - return Badge( - shape: BadgeShape.square, - borderRadius: BorderRadius.all(Radius.circular(5)), - padding: EdgeInsets.symmetric( - horizontal: 5, - vertical: 2, - ), - badgeContent: Text( - '$count', - style: TextStyle( - color: Colors.white, - fontSize: Dim.tm2(), - ), - ), - ); - else - return Container(); - }, - ) - ], - ), ], ), ), diff --git a/lib/widgets/common/channel_title.dart b/lib/widgets/common/channel_title.dart new file mode 100644 index 00000000..eae7be73 --- /dev/null +++ b/lib/widgets/common/channel_title.dart @@ -0,0 +1,43 @@ +import 'package:meta/meta.dart'; +import 'package:flutter/material.dart'; + +class ChannelTitle extends StatelessWidget { + final String name; + final bool isPrivate; + final bool hasUnread; + + const ChannelTitle({ + Key key, + @required this.name, + @required this.isPrivate, + this.hasUnread = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + style: TextStyle( + fontSize: 17.0, + fontWeight: hasUnread ? FontWeight.w900 : FontWeight.w400, + color: Color(0xff444444), + ), + children: [ + TextSpan(text: name), + WidgetSpan(child: SizedBox(width: 6)), + if (isPrivate) + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + Icons.lock_outline, + size: 16.0, + color: Color(0xff444444), + ), + ), + ], + ), + overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.start, + ); + } +} diff --git a/lib/widgets/message/message_tile.dart b/lib/widgets/message/message_tile.dart index 2847026f..8687126d 100644 --- a/lib/widgets/message/message_tile.dart +++ b/lib/widgets/message/message_tile.dart @@ -1,7 +1,6 @@ import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:twake/blocs/base_channel_bloc/base_channel_bloc.dart'; import 'package:twake/blocs/directs_bloc/directs_bloc.dart'; import 'package:twake/blocs/draft_bloc/draft_bloc.dart'; @@ -22,6 +21,7 @@ import 'package:twake/widgets/common/image_avatar.dart'; import 'package:twake/widgets/common/reaction.dart'; import 'package:twake/widgets/message/message_modal_sheet.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; final RegExp singleLineFeed = RegExp('(? { super.initState(); // First field init context.read().add( - RemovableTextField( + field: RemovableTextField( key: UniqueKey(), index: 0, isLastOne: true, ), - 0); + atIndex: 0); } void _return() { diff --git a/lib/widgets/sheets/add/workspace_info_form.dart b/lib/widgets/sheets/add/workspace_info_form.dart index 8a6e315a..72708af8 100644 --- a/lib/widgets/sheets/add/workspace_info_form.dart +++ b/lib/widgets/sheets/add/workspace_info_form.dart @@ -114,7 +114,8 @@ class _WorkspaceInfoFormState extends State { bool createIsBlocked = state is Creation; if (state is Updated) { _collaborators = state.repository?.members; - // print('Collaborators: $_collaborators'); + + print('Collaborators: $_collaborators'); } return BlocListener( listener: (context, state) { diff --git a/lib/widgets/sheets/removable_text_field.dart b/lib/widgets/sheets/removable_text_field.dart index 3b0fee17..f3b16f80 100644 --- a/lib/widgets/sheets/removable_text_field.dart +++ b/lib/widgets/sheets/removable_text_field.dart @@ -25,7 +25,6 @@ class _RemovableTextFieldState extends State { var _isLastOne = false; var _index = 0; var _inFocus = false; - var _editable = true; @override void initState() { @@ -37,8 +36,12 @@ class _RemovableTextFieldState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { _controller.addListener(() { String text = _controller.text; + print('Update text: $text'); + print('Update at index: $_index'); if (text.isNotReallyEmpty) { - context.read().update(_index, text); + context + .read() + .update(withContent: text, atIndex: _index); } }); }); @@ -63,20 +66,20 @@ class _RemovableTextFieldState extends State { void _add() { context.read().add( - RemovableTextField( - key: UniqueKey(), - index: _index + 1, - isLastOne: true, - ), - _index + 1, - ); + field: RemovableTextField( + key: UniqueKey(), + index: _index + 1, + isLastOne: true, + ), + atIndex: _index + 1, + ); setState(() { _isLastOne = false; }); } void _remove() { - context.read().remove(_index); + context.read().remove(atIndex: _index); } @override @@ -137,7 +140,7 @@ class _RemovableTextFieldState extends State { child: Icon( CupertinoIcons.clear_thick_circled, color: - _inFocus ? Colors.grey : Colors.transparent, + _inFocus ? Colors.grey : Colors.transparent, size: 20, ), ), @@ -165,4 +168,4 @@ class _RemovableTextFieldState extends State { ), ); } -} \ No newline at end of file +} diff --git a/log.sh b/log.sh deleted file mode 100755 index e9bd1009..00000000 --- a/log.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -DATE_WEEK="2021-01-18T00:00:00-07:00" -DATE_MONTH="2021-01-01T00:00:00-07:00" - -function process { - echo $1 - printf "Commits (week):" - git log --oneline --author=$2 --since="$DATE_WEEK" | wc -l - printf "Commits (month):" - git log --oneline --author=$2 --since="$DATE_MONTH" | wc -l - printf "loc (week):" - git log --pretty=tformat: --numstat --author=$2 --since="$DATE_WEEK" \ - | gawk '{ add += $1; subs += $2; loc += $1 + $2 } END { printf "added lines: %s removed lines: %s total lines: %s\n", add, subs, loc }' - - printf "loc (month):" - git log --pretty=tformat: --numstat --author=$2 --since="$DATE_MONTH" \ - | gawk '{ add += $1; subs += $2; loc += $1 + $2 } END { printf "added lines: %s removed lines: %s total lines: %s\n", add, subs, loc }' - - echo "-------" -} - -process 'Romaric Mourgues' 'rmourgues@linagora.com' -process 'Benoit Tallandier' 'benoit@twakeapp.com' -process 'Stéphane Vieira' '36481167+stephanevieira75@users.noreply.github.com' -process 'Christohpe Hamerling' 'christophe.hamerling@gmail.com' -process "Aiman R'Kyek" 'RkAiman' -process "Titouan Issarni" 'tissarni' -process 'Roman Bykovsky' '8026787@gmail.com' -process 'Babur Makhmudov' 'bobs4462' -process 'Pavel Zarudnev' 'rockinpaulz@gmail.com' diff --git a/pubspec.yaml b/pubspec.yaml index 30fd5e00..b68f0aaf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.2.9+4 +version: 2.2.9+5 environment: sdk: ">=2.7.0 <3.0.0" @@ -23,52 +23,54 @@ environment: dependencies: flutter: sdk: flutter - json_annotation: ^3.1.0 # serialization/deserialization auto code generation - dio: ^3.0.10 + json_annotation: ^4.0.1 # serialization/deserialization auto code generation + dio: ^4.0.0 intl: ^0.16.1 sembast: ^2.4.8 - path_provider: ^1.6.24 + path_provider: ^2.0.1 sticky_grouped_list: ^1.3.0 scrollable_positioned_list: ^0.1.8 - flutter_inappwebview: ^4.0.0+4 + flutter_inappwebview: ^5.3.0 webview_flutter: ^1.0.7 - logger: ^0.9.4 + logger: ^1.0.0 clipboard: ^0.1.2+8 - url_launcher: ^5.7.10 - package_info: ^0.4.3+2 - lottie: ^0.7.0+1 + url_launcher: ^6.0.3 + package_info: ^2.0.0 + lottie: ^1.0.1 sqflite: ^1.3.2+2 tuple: ^1.0.3 # Uncomment if sentry is needed - sentry_flutter: ^4.0.4 - firebase_messaging: ^7.0.3 + sentry_flutter: ^4.0.6 + firebase_core: ^1.0.2 + firebase_messaging: ^9.1.0 equatable: ^1.2.5 flutter_bloc: ^6.1.1 - image_picker: ^0.6.7+21 - connectivity: ^2.0.2 + image_picker: ^0.7.3 + connectivity: ^3.0.3 sliding_up_panel: ^1.0.2 - socket_io_client: ^0.9.12 + # socket_io_client: ^2.0.0-beta.3-nullsafety.0 + socket_io_client: + git: https://github.com/bobs4462/socket.io-client-dart.git auto_size_text: ^2.1.0 badges: ^1.2.0 flutter_emoji_keyboard: # path: ../emoji_keyboard git: https://github.com/bobs4462/emoji_keyboard.git + flutter_markdown: ^0.6.1 - flutter_markdown: - git: https://github.com/bobs4462/flutter_markdown.git - - flutter_local_notifications: ^4.0.1+1 + flutter_local_notifications: ^5.0.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.0 dev_dependencies: - flutter_test: - sdk: flutter + # flutter_test: + # sdk: flutter build_runner: ^1.10.4 - json_serializable: ^3.5.0 - flutter_launcher_icons: ^0.8.1 + json_serializable: ^4.1.0 + flutter_launcher_icons: ^0.9.0 + test: ^1.15.8 #dependency_overrides: # sqflite: 1.3.0 diff --git a/test/twacode_test.dart b/test/twacode_test.dart index 17ffe96b..5c3c0ca4 100644 --- a/test/twacode_test.dart +++ b/test/twacode_test.dart @@ -1,196 +1,138 @@ -import 'package:flutter/material.dart' hide Text; -import 'package:flutter_test/flutter_test.dart'; -// import 'package:twake_mobile/utils/twacode.dart'; +import 'package:twake/utils/twacode.dart'; +import 'package:test/test.dart'; +void main() { + test("Should parse plain text", () { + final data = "This is just a normal text"; + final parsed = TwacodeParser(data); + expect(parsed.message, ["This is just a normal text"]); + }); -// class MockBuildContext extends Mock implements BuildContext {} + test("Should parse text with line breaks", () { + final data = "This is a text\nAnd a new line"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "This is a text", + {"start": "", "end": "\n", "content": const []}, + "And a new line" + ]); + }); -void main() { - // // MockBuildContext _mockContext; - // List parsed; - // - // setUp(() { - // // _mockContext = MockBuildContext(); - // }); - // - // test('Parser', () { - // var testString = [ - // {"type": "user", "content": "tuanpham", "id": "568f3f08-6e78-11ea-b65f-0242ac120004"}, - // {"type": "text", "content": "text"}, - // {"type": "url", "content": "https://ci.linagora.com/linagora/lgs/common-tools/dockerfiles/lemonldap-generic"}, - // {"type": "underline", "content": " underlined"}, - // {"type": "strikethrough", "content": " strikethrough"}, - // {"type": "bold", "content": "bold"}, - // {"type": "italic", "content": "italic"}, - // {"type": "mcode", "content": "mcode"}, - // {"type": "icode", "content": "icode"}, - // {"type": "mquote", "content": "mquote"}, - // {"type": "quote", "content": "quote"}, - // {"type": "channel", "content": "channel"}, - // {"type": "compile", "content": "compile"}, - // {"type": "email", "content": "email"}, - // {"type": "system", "content": "system"}, - // {"type": "image", "content": "image"}, - // {"type": "emoji", "content": "emoji"}, - // {"type": "icon", "content": "icon"}, - // {"type": "copiable", "content": "copiable"}, - // {"type": "br"}, - // {"type": "attachment", "content": [{}]}, - // {"type": "progress_bar", "content": 42} - // ]; - // - // var parsed = Parser(testString); - // - // parsed.items.forEach((element) { - // switch (element.type) { - // case TwacodeType.text: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, defaultColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.none); - // break; - // case TwacodeType.bold: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.bold); - // expect(widget.style.color, defaultColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.none); - // break; - // case TwacodeType.italic: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, defaultColor); - // expect(widget.style.fontStyle, FontStyle.italic); - // expect(widget.style.decoration, TextDecoration.none); - // break; - // case TwacodeType.underline: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, defaultColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.underline); - // break; - // case TwacodeType.strikethrough: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, defaultColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.lineThrough); - // break; - // case TwacodeType.url: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, linkColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.none); - // expect(widget.recognizer, isNot(null)); - // break; - // case TwacodeType.user: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, linkColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.none); - // expect(widget.recognizer, isNot(null)); - // break; - // case TwacodeType.channel: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, linkColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.none); - // expect(widget.recognizer, isNot(null)); - // break; - // case TwacodeType.email: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, linkColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.none); - // expect(widget.recognizer, isNot(null)); - // break; - // case TwacodeType.image: - // WidgetSpan widget = element.render(); - // expect(widget.child.runtimeType , Image); - // break; - // case TwacodeType.br: - // TextSpan widget = element.render(); - // expect(widget.text, '\n'); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, defaultColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.none); - // break; - // - // case TwacodeType.icode: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, codeColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.none); - // break; - // case TwacodeType.mcode: - // TextSpan widget = element.render(); - // expect(widget.text, '\n' + element.content + '\n'); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, codeColor); - // expect(widget.style.fontStyle, FontStyle.normal); - // expect(widget.style.decoration, TextDecoration.none); - // break; - // case TwacodeType.quote: - // TextSpan widget = element.render(); - // expect(widget.text, element.content); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, quoteColor); - // expect(widget.style.fontStyle, FontStyle.italic); - // expect(widget.style.decoration, TextDecoration.none); - // break; - // case TwacodeType.mquote: - // TextSpan widget = element.render(); - // expect(widget.text, '\n' + element.content + '\n'); - // expect(widget.style.fontWeight, FontWeight.normal); - // expect(widget.style.color, quoteColor); - // expect(widget.style.fontStyle, FontStyle.italic); - // expect(widget.style.decoration, TextDecoration.none); - // break; - // case TwacodeType.emoji: - // // TODO: test needed - // break; - // case TwacodeType.compile: - // // TODO: test needed - // break; - // case TwacodeType.icon: - // // TODO: test needed - // break; - // case TwacodeType.copiable: - // // TODO: test needed - // break; - // case TwacodeType.system: - // // TODO: test needed - // break; - // case TwacodeType.attachment: - // // TODO: test needed - // break; - // case TwacodeType.compile: - // // TODO: test needed - // break; - // case TwacodeType.progress_bar: - // // TODO: test needed - // break; - // default: - // throw ("No test for type " + element.type.toString()); - // } - // }); - // }); + test("Should parse bold", () { + final data = "Hello **stranger**."; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "Hello ", + {"start": "**", "end": "**", "content": "stranger"}, + "." + ]); + }); + + test("Should parse italic", () { + final data = "I am _italic_, did you know that?"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "I am ", + {"start": "_", "end": "_", "content": "italic"}, + ", did you know that?" + ]); + }); + + test("Should parse underline", () { + final data = "This is very __important thing__ to write!"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "This is very ", + {"start": "__", "end": "__", "content": "important thing"}, + " to write!" + ]); + }); + + test("Should parse strikethrough", () { + final data = "Ooops, ~~you didn't see that~~ :)"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "Ooops, ", + {"start": "~~", "end": "~~", "content": "you didn't see that"}, + " :)" + ]); + }); + + test("Should parse quote", () { + final data = "> This is a famous quote"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + {"start": ">", "content": " This is a famous quote"} + ]); + }); + + test("Should parse inline code", () { + final data = "Inline follows `int main (void) {};`"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "Inline follows ", + {"start": "`", "end": "`", "content": "int main (void) {};"} + ]); + }); + + test("Should parse multiline code", () { + final data = """Multiline code: +``` +print("Hello world") +inp = input("> ") +print(f"Input {inp}") + +exit(0) +```"""; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "Multiline code:", + {"start": "", "end": "\n", "content": const []}, + { + "start": "```", + "end": "```", + "content": + "\nprint(\"Hello world\")\ninp = input(\"> \")\nprint(f\"Input {inp}\")\n\nexit(0)\n" + } + ]); + }); + + test("Should parse user", () { + final data = "Hello @stranger:5268fa80-19d2-11eb-b774-0242ac120004 :)"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "Hello ", + { + "start": "@", + "content": "stranger:5268fa80-19d2-11eb-b774-0242ac120004" + }, + " :)", + ]); + }); + test("Should parse channel", () { + final data = "I'm here: #channel"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "I'm here: ", + {"start": "#", "content": "channel"} + ]); + }); + + test("Should parse url", () { + final data = "My site: http://hello.world.com"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "My site: ", + {"type": "url", "content": "http://hello.world.com"} + ]); + }); + + test("Should parse email", () { + final data = "My email: hello@world.com"; + final parsed = TwacodeParser(data); + expect(parsed.message, [ + "My email: ", + {"type": "email", "content": "hello@world.com"} + ]); + }); }