diff --git a/.gitignore b/.gitignore index d48397dd..87ca5f57 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ app.*.symbols app.*.map.json /ios/Runner/GoogleService-Info.plist /android/app/google-services.json +/android/key.properties +/android/android.jks diff --git a/android/app/build.gradle b/android/app/build.gradle index 85f860d4..ee0aeecc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,6 +25,12 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + android { compileSdkVersion 29 @@ -46,11 +52,17 @@ android { versionName flutterVersionName } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig signingConfigs.release } } } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..0f96ad8a --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,23 @@ +## Gson rules +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} diff --git a/android/app/src/main/res/drawable-hdpi/logo_blue.png b/android/app/src/main/res/drawable-hdpi/logo_blue.png new file mode 100644 index 00000000..fb2a466c Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/logo_blue.png differ diff --git a/android/app/src/main/res/drawable-mdpi/logo_blue.png b/android/app/src/main/res/drawable-mdpi/logo_blue.png new file mode 100644 index 00000000..fb2a466c Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/logo_blue.png differ diff --git a/android/app/src/main/res/drawable-v21/logo_blue.png b/android/app/src/main/res/drawable-v21/logo_blue.png new file mode 100644 index 00000000..fb2a466c Binary files /dev/null and b/android/app/src/main/res/drawable-v21/logo_blue.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/logo_blue.png b/android/app/src/main/res/drawable-xhdpi/logo_blue.png new file mode 100644 index 00000000..fb2a466c Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/logo_blue.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/logo_blue.png b/android/app/src/main/res/drawable-xxhdpi/logo_blue.png new file mode 100644 index 00000000..fb2a466c Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/logo_blue.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/logo_blue.png b/android/app/src/main/res/drawable-xxxhdpi/logo_blue.png new file mode 100644 index 00000000..fb2a466c Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/logo_blue.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index f114f65c..c974a4f6 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -9,4 +9,9 @@ android:gravity="center" android:src="@drawable/logo" /> + + + diff --git a/android/app/src/main/res/drawable/logo_blue.png b/android/app/src/main/res/drawable/logo_blue.png new file mode 100644 index 00000000..fb2a466c Binary files /dev/null and b/android/app/src/main/res/drawable/logo_blue.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0131e946..38bbeea7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -47,6 +47,8 @@ PODS: - Flutter (1.0.0) - flutter_inappwebview (0.0.1): - Flutter + - flutter_local_notifications (0.0.1): + - Flutter - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) @@ -104,6 +106,7 @@ DEPENDENCIES: - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) @@ -142,6 +145,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_inappwebview: :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" image_picker: :path: ".symlinks/plugins/image_picker/ios" package_info: @@ -170,6 +175,7 @@ SPEC CHECKSUMS: FirebaseMessaging: 5eca4ef173de76253352511aafef774caa1cba2a Flutter: 0e3d915762c693b495b44d77113d4970485de6ec flutter_inappwebview: 69dfbac46157b336ffbec19ca6dfd4638c7bf189 + flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833 GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3 diff --git a/lib/blocs/add_channel_bloc/add_channel_bloc.dart b/lib/blocs/add_channel_bloc/add_channel_bloc.dart index df90467f..a3b899f3 100644 --- a/lib/blocs/add_channel_bloc/add_channel_bloc.dart +++ b/lib/blocs/add_channel_bloc/add_channel_bloc.dart @@ -1,13 +1,18 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:twake/repositories/add_channel_repository.dart'; +import 'package:twake/repositories/add_direct_repository.dart'; import 'add_channel_event.dart'; import 'add_channel_state.dart'; class AddChannelBloc extends Bloc { - final AddChannelRepository repository; + final AddChannelRepository channelRepository; + final AddDirectRepository directRepository; - AddChannelBloc(this.repository) : super(AddChannelInitial()); + AddChannelBloc( + this.channelRepository, + this.directRepository, + ) : super(AddChannelInitial()); @override Stream mapEventToState( @@ -16,43 +21,64 @@ class AddChannelBloc extends Bloc { if (event is SetFlowStage) { yield StageUpdated(event.stage); } else if (event is Update) { - repository.icon = event.icon ?? repository.icon; - repository.name = event.name ?? repository.name; - repository.description = event.description ?? repository.description; - repository.channelGroup = event.groupName ?? repository.channelGroup; - repository.type = event.type ?? repository.type ?? ChannelType.public; - repository.members = event.participants ?? repository.members ?? []; - repository.def = event.automaticallyAddNew ?? repository.def ?? true; + channelRepository.icon = event.icon ?? channelRepository.icon; + channelRepository.name = event.name ?? channelRepository.name; + channelRepository.description = + event.description ?? channelRepository.description; + channelRepository.channelGroup = + event.groupName ?? channelRepository.channelGroup; + channelRepository.type = + event.type ?? channelRepository.type ?? ChannelType.public; + channelRepository.members = + event.participants ?? channelRepository.members ?? []; + channelRepository.def = + event.automaticallyAddNew ?? channelRepository.def ?? true; // print('Updated data: ${repository.toJson()}'); - var newRepo = AddChannelRepository( - icon: repository.icon, - companyId: repository.companyId, - workspaceId: repository.workspaceId, - name: repository.name, - visibility: repository.visibility, - description: repository.description, - channelGroup: repository.channelGroup, - type: repository.type, - members: repository.members, - def: repository.def, + final newRepo = AddChannelRepository( + icon: channelRepository.icon, + companyId: channelRepository.companyId, + workspaceId: channelRepository.workspaceId, + name: channelRepository.name, + visibility: channelRepository.visibility, + description: channelRepository.description, + channelGroup: channelRepository.channelGroup, + type: channelRepository.type, + members: channelRepository.members, + def: channelRepository.def, ); - yield Updated(newRepo); + } else if (event is UpdateDirect) { + directRepository.member = event.member; + // print('Updated data: ${repository.toJson()}'); + final newRepo = AddDirectRepository( + companyId: directRepository.companyId, + workspaceId: directRepository.workspaceId, + member: directRepository.member, + ); + yield DirectUpdated(newRepo); } else if (event is Create) { yield Creation(); - final type = repository.type; - final result = await repository.create(); + final type = channelRepository.type; + final result = await channelRepository.create(); if (result.isNotEmpty) { - repository.clear(); - yield Created(result, type); + channelRepository.clear(); + yield Created(result, channelType: type); } else { yield Error('Channel creation failure!'); } } else if (event is Clear) { - repository.clear(); + channelRepository.clear(); } else if (event is SetFlowType) { yield FlowTypeSet(event.isDirect); + } else if (event is CreateDirect) { + yield Creation(); + final result = await directRepository.create(); + if (result.isNotEmpty) { + yield DirectCreated(result); + } else { + yield Error('Direct creation failure!'); + } } } } diff --git a/lib/blocs/add_channel_bloc/add_channel_event.dart b/lib/blocs/add_channel_bloc/add_channel_event.dart index f71c7100..f164c270 100644 --- a/lib/blocs/add_channel_bloc/add_channel_event.dart +++ b/lib/blocs/add_channel_bloc/add_channel_event.dart @@ -15,6 +15,11 @@ class Create extends AddChannelEvent { List get props => []; } +class CreateDirect extends AddChannelEvent { + @override + List get props => []; +} + class Update extends AddChannelEvent { final String icon; final String name; @@ -46,6 +51,15 @@ class Update extends AddChannelEvent { ]; } +class UpdateDirect extends AddChannelEvent { + final String member; + + UpdateDirect({this.member}); + + @override + List get props => [member]; +} + class SetFlowType extends AddChannelEvent { final bool isDirect; diff --git a/lib/blocs/add_channel_bloc/add_channel_state.dart b/lib/blocs/add_channel_bloc/add_channel_state.dart index 41916813..07419ea8 100644 --- a/lib/blocs/add_channel_bloc/add_channel_state.dart +++ b/lib/blocs/add_channel_bloc/add_channel_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:twake/repositories/add_channel_repository.dart'; +import 'package:twake/repositories/add_direct_repository.dart'; abstract class AddChannelState extends Equatable { const AddChannelState(); @@ -19,6 +20,15 @@ class Updated extends AddChannelState { List get props => [repository]; } +class DirectUpdated extends AddChannelState { + final AddDirectRepository repository; + + DirectUpdated(this.repository); + + @override + List get props => [repository]; +} + class Creation extends AddChannelState { @override List get props => []; @@ -28,12 +38,21 @@ class Created extends AddChannelState { final String id; final ChannelType channelType; - Created(this.id, this.channelType); + Created(this.id, {this.channelType}); @override List get props => [id, channelType]; } +class DirectCreated extends AddChannelState { + final String id; + + DirectCreated(this.id); + + @override + List get props => [id]; +} + class Error extends AddChannelState { final String message; diff --git a/lib/blocs/auth_bloc/auth_bloc.dart b/lib/blocs/auth_bloc/auth_bloc.dart index 2a52b295..677dd480 100644 --- a/lib/blocs/auth_bloc/auth_bloc.dart +++ b/lib/blocs/auth_bloc/auth_bloc.dart @@ -123,10 +123,16 @@ class AuthBloc extends Bloc { ); switch (res) { case AuthResult.WrongCredentials: - yield WrongCredentials(); + yield WrongCredentials( + username: event.username, + password: event.password, + ); break; case AuthResult.NetworkError: - yield AuthenticationError(); + yield AuthenticationError( + username: event.username, + password: event.password, + ); break; default: final InitData initData = await initMain(); diff --git a/lib/blocs/auth_bloc/auth_state.dart b/lib/blocs/auth_bloc/auth_state.dart index cf1253cb..96e30707 100644 --- a/lib/blocs/auth_bloc/auth_state.dart +++ b/lib/blocs/auth_bloc/auth_state.dart @@ -14,15 +14,18 @@ class AuthInitializing extends AuthState { class Unauthenticated extends AuthState { final String message; + final String username; + final String password; - const Unauthenticated({this.message}); + const Unauthenticated({this.message, this.username, this.password}); @override - List get props => [message]; + List get props => [username, password]; } class WrongCredentials extends Unauthenticated { - const WrongCredentials(); + const WrongCredentials({String username, String password}) + : super(username: username, password: password); @override List get props => []; @@ -62,8 +65,9 @@ class PasswordReset extends AuthState { List get props => [link]; } -class AuthenticationError extends AuthState { - const AuthenticationError(); +class AuthenticationError extends Unauthenticated { + const AuthenticationError({String username, String password}) + : super(username: username, password: password); @override List get props => []; diff --git a/lib/blocs/base_channel_bloc/base_channel_bloc.dart b/lib/blocs/base_channel_bloc/base_channel_bloc.dart index 4e03d105..57fbc76a 100644 --- a/lib/blocs/base_channel_bloc/base_channel_bloc.dart +++ b/lib/blocs/base_channel_bloc/base_channel_bloc.dart @@ -27,25 +27,25 @@ abstract class BaseChannelBloc extends Bloc { Future updateMessageCount(ModifyMessageCount event) async { final ch = await repository.getItemById(event.channelId); if (ch != null) { + repository.logger.d("UPDATING CHANNEL UNREAD"); // ch.messagesTotal += event.totalModifier ?? 0; ch.hasUnread = 1; ch.messagesUnread += event.unreadModifier ?? 0; ch.lastActivity = event.timeStamp ?? DateTime.now().millisecondsSinceEpoch; repository.saveOne(ch); - } else - return; + } } Future updateChannelState(ModifyChannelState event) async { final ch = await repository.getItemById(event.channelId); + repository.logger.d("UPDATING CHANNEL STATE"); if (ch != null) { ch.hasUnread = 1; if (event.threadId != null || event.messageId != null) { ch.messagesUnread += 1; } repository.saveOne(ch); - } else - return; + } } } diff --git a/lib/blocs/channels_bloc/channels_bloc.dart b/lib/blocs/channels_bloc/channels_bloc.dart index b0a62b86..505f6804 100644 --- a/lib/blocs/channels_bloc/channels_bloc.dart +++ b/lib/blocs/channels_bloc/channels_bloc.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:twake/blocs/base_channel_bloc/base_channel_bloc.dart'; import 'package:twake/blocs/notification_bloc/notification_bloc.dart'; +import 'package:twake/blocs/profile_bloc/profile_bloc.dart'; import 'package:twake/blocs/workspaces_bloc/workspaces_bloc.dart'; import 'package:twake/blocs/channels_bloc/channel_event.dart'; import 'package:twake/models/channel.dart'; @@ -9,6 +10,7 @@ import 'package:twake/models/channel.dart'; import 'package:twake/repositories/collection_repository.dart'; import 'package:twake/blocs/channels_bloc/channel_state.dart'; import 'package:twake/blocs/workspaces_bloc/workspace_state.dart'; +import 'package:twake/services/endpoints.dart'; export 'package:twake/blocs/channels_bloc/channel_event.dart'; export 'package:twake/blocs/channels_bloc/channel_state.dart'; @@ -106,7 +108,14 @@ class ChannelsBloc extends BaseChannelBloc { yield ChannelsEmpty(); } else if (event is ChangeSelectedChannel) { repository.logger.w('CHANNEL ${event.channelId} is selected'); - repository.select(event.channelId, saveToStore: false); + repository.select(event.channelId, + saveToStore: false, + apiEndpoint: Endpoint.channelsRead, + params: { + "company_id": ProfileBloc.selectedCompany, + "workspace_id": ProfileBloc.selectedWorkspace, + "channel_id": event.channelId + }); repository.selected.messagesUnread = 0; repository.selected.hasUnread = 0; @@ -116,6 +125,8 @@ class ChannelsBloc extends BaseChannelBloc { selected: repository.selected, hasUnread: repository.selected.hasUnread, ); + ProfileBloc.selectedChannel = event.channelId; + ProfileBloc.selectedThread = null; yield newState; } else if (event is ModifyMessageCount) { await this.updateMessageCount(event); diff --git a/lib/blocs/directs_bloc/directs_bloc.dart b/lib/blocs/directs_bloc/directs_bloc.dart index a322f345..13aa2e22 100644 --- a/lib/blocs/directs_bloc/directs_bloc.dart +++ b/lib/blocs/directs_bloc/directs_bloc.dart @@ -4,9 +4,11 @@ import 'package:twake/blocs/base_channel_bloc/base_channel_bloc.dart'; import 'package:twake/blocs/companies_bloc/companies_bloc.dart'; import 'package:twake/blocs/notification_bloc/notification_bloc.dart'; import 'package:twake/blocs/channels_bloc/channel_event.dart'; +import 'package:twake/blocs/profile_bloc/profile_bloc.dart'; import 'package:twake/models/direct.dart'; import 'package:twake/repositories/collection_repository.dart'; import 'package:twake/blocs/channels_bloc/channel_state.dart'; +import 'package:twake/services/service_bundle.dart'; export 'package:twake/blocs/channels_bloc/channel_event.dart'; export 'package:twake/blocs/channels_bloc/channel_state.dart'; @@ -101,7 +103,14 @@ class DirectsBloc extends BaseChannelBloc { await repository.clean(); yield ChannelsEmpty(); } else if (event is ChangeSelectedChannel) { - repository.select(event.channelId); + repository.select(event.channelId, + saveToStore: false, + apiEndpoint: Endpoint.channelsRead, + params: { + "company_id": ProfileBloc.selectedCompany, + "workspace_id": "direct", + "channel_id": event.channelId + }); repository.selected.messagesUnread = 0; repository.selected.hasUnread = 0; repository.saveOne(repository.selected); diff --git a/lib/blocs/messages_bloc/messages_bloc.dart b/lib/blocs/messages_bloc/messages_bloc.dart index 4b59f849..21d77f8f 100644 --- a/lib/blocs/messages_bloc/messages_bloc.dart +++ b/lib/blocs/messages_bloc/messages_bloc.dart @@ -86,16 +86,16 @@ class MessagesBloc if (T == DirectsBloc && state.data.workspaceId == 'direct') { while (selectedChannel.id != state.data.channelId || this.state is! MessagesLoaded) { - print('Waiting for the correct channel loading\n' - 'COND1: ${selectedChannel.id != state.data.channelId}\n' - 'COND2: ${this.state is! MessagesLoaded}'); + // print('Waiting for the correct channel loading\n' + // 'COND1: ${selectedChannel.id != state.data.channelId}\n' + // 'COND2: ${this.state is! MessagesLoaded}'); await Future.delayed(Duration(milliseconds: 500)); } this.add(SelectMessage(state.data.threadId)); } else if (T == ChannelsBloc && state.data.workspaceId != 'direct') { while (selectedChannel.id != state.data.channelId || this.state is! MessagesLoaded) { - print('Waiting for the correct channel loading'); + // print('Waiting for the correct channel loading'); await Future.delayed(Duration(milliseconds: 500)); } this.add(SelectMessage(state.data.threadId)); @@ -197,25 +197,25 @@ class MessagesBloc _makeQueryParams(event), addToItems: event.channelId == selectedChannel.id, ); + _sortItems(); if (updateParent) { - _sortItems(); - final newState = MessagesLoaded( - messages: repository.items, - messageCount: repository.itemsCount, - force: DateTime.now().toString(), - parentChannel: selectedChannel, - ); - yield newState; - _updateParentChannel(); + _updateParentChannel(event.channelId); } + final newState = MessagesLoaded( + messages: repository.items, + messageCount: repository.itemsCount, + force: DateTime.now().toString(), + parentChannel: selectedChannel, + ); + yield newState; } else if (event is ModifyResponsesCount) { var thread = await repository.updateResponsesCount(event.threadId); if (thread == null) return; if (repository.selected == null) return; if (event.channelId == selectedChannel.id) { - repository.logger - .d('In thread: ${event.threadId == repository.selected.id}'); + // repository.logger + // .d('In thread: ${event.threadId == repository.selected.id}'); thread = event.threadId == repository.selected.id ? thread : repository.selected; @@ -226,9 +226,9 @@ class MessagesBloc parentChannel: selectedChannel, force: DateTime.now().toString(), ); - repository.logger.d('YIELDING STATE: ${newState != this.state}'); + // repository.logger.d('YIELDING STATE: ${newState != this.state}'); yield newState; - _updateParentChannel(); + // _updateParentChannel(); } } else if (event is RemoveMessage) { final channelId = event.channelId ?? selectedChannel.id; @@ -287,7 +287,7 @@ class MessagesBloc message.lastName = ProfileBloc.lastName; this.repository.items.add(message); this.add(FinishLoadingMessages()); - _updateParentChannel(); + // _updateParentChannel(); }, ); this.repository.items.add(tempItem); @@ -307,7 +307,8 @@ class MessagesBloc parentChannel: selectedChannel, ); } else if (event is SelectMessage) { - print('$T MESSAGE SELECTED'); + // print('$T MESSAGE SELECTED'); + ProfileBloc.selectedThread = event.messageId; repository.select(event.messageId); yield MessageSelected( threadMessage: repository.selected, @@ -316,7 +317,6 @@ class MessagesBloc parentChannel: selectedChannel, ); await repository.updateResponsesCount(event.messageId); - print('SELECTED THREAD IS ${repository.selected.id}'); yield MessageSelected( threadMessage: repository.selected, responsesCount: repository.selected.responsesCount, @@ -342,10 +342,10 @@ class MessagesBloc return map; } - void _updateParentChannel() { + void _updateParentChannel([String channelId]) { channelsBloc.add(ModifyMessageCount( workspaceId: ProfileBloc.selectedWorkspace, - channelId: selectedChannel.id, + channelId: channelId ?? selectedChannel.id, companyId: ProfileBloc.selectedCompany, totalModifier: 1, )); diff --git a/lib/blocs/notification_bloc/notification_bloc.dart b/lib/blocs/notification_bloc/notification_bloc.dart index fdae13ab..294fe25a 100644 --- a/lib/blocs/notification_bloc/notification_bloc.dart +++ b/lib/blocs/notification_bloc/notification_bloc.dart @@ -52,6 +52,7 @@ class NotificationBloc extends Bloc { onMessageCallback: onMessageCallback, onResumeCallback: onResumeCallback, onLaunchCallback: onLaunchCallback, + shouldNotify: shouldNotify, ); print('TOKEN: $token\nHOST: $socketIOHost'); socket = IO.io( @@ -95,8 +96,8 @@ class NotificationBloc extends Bloc { socketConnectionState = SocketConnectionState.CONNECTED; while (socketConnectionState != SocketConnectionState.AUTHENTICATED) { if (socket.disconnected) socket = socket.connect(); - await Future.delayed(Duration(seconds: 2)); socket.emit(SocketIOEvent.AUTHENTICATE, {'token': this.token}); + await Future.delayed(Duration(seconds: 5)); print('WAITING FOR SOCKET AUTH'); } }); @@ -192,8 +193,14 @@ class NotificationBloc extends Bloc { } } + bool shouldNotify(MessageNotification data) { + if (data.channelId == ProfileBloc.selectedChannel && + (ProfileBloc.selectedThread == data.threadId || + ProfileBloc.selectedThread == null)) return false; + return true; + } + void onMessageCallback(NotificationData data) { - logger.d('ON message callback: ${data is MessageNotification}'); if (data is MessageNotification) { if (data.threadId.isNotEmpty && data.threadId != data.messageId) { logger.d('adding ThreadMessageEvent'); @@ -206,6 +213,7 @@ class NotificationBloc extends Bloc { this.add(ChannelMessageEvent(data)); } } + navigate(data); // } else if (data is WhatsNewItem) { // if (data.workspaceId == null) { // this.add(UpdateDirectChannel(data)); @@ -215,8 +223,11 @@ class NotificationBloc extends Bloc { // } } - Future onResumeCallback(MessageNotification data) async { + onResumeCallback(MessageNotification data) { onMessageCallback(data); + } + + void navigate(MessageNotification data) { navigator.currentState.popUntil( ModalRoute.withName('/'), ); // navigator.popAndPushNamed( @@ -246,7 +257,6 @@ class NotificationBloc extends Bloc { ), ); } - // logger.w('ON RESUME HERE IS the notification\n$data'); } void onLaunchCallback(NotificationData data) { diff --git a/lib/blocs/profile_bloc/profile_bloc.dart b/lib/blocs/profile_bloc/profile_bloc.dart index 1cf15517..d76748e1 100644 --- a/lib/blocs/profile_bloc/profile_bloc.dart +++ b/lib/blocs/profile_bloc/profile_bloc.dart @@ -32,6 +32,9 @@ class ProfileBloc extends Bloc { static String get selectedCompany => repository.selectedCompanyId; static String get selectedWorkspace => repository.selectedWorkspaceId; + static String get selectedChannel => repository.selectedChannelId; + static String get selectedThread => repository.selectedThreadId; + static set selectedCompany(String val) { repository.selectedCompanyId = val; repository.save(); @@ -42,6 +45,14 @@ class ProfileBloc extends Bloc { repository.save(); } + static set selectedChannel(String val) { + repository.selectedChannelId = val; + } + + static set selectedThread(String val) { + repository.selectedThreadId = val; + } + @override Stream mapEventToState(ProfileEvent event) async* { if (event is ReloadProfile) { diff --git a/lib/blocs/single_message_bloc/single_message_bloc.dart b/lib/blocs/single_message_bloc/single_message_bloc.dart index 4d723b97..c944f0c2 100644 --- a/lib/blocs/single_message_bloc/single_message_bloc.dart +++ b/lib/blocs/single_message_bloc/single_message_bloc.dart @@ -42,7 +42,7 @@ class SingleMessageBloc extends Bloc { message.updateContent({ 'company_id': ProfileBloc.selectedCompany, 'channel_id': message.channelId, - 'workspace_id': ProfileBloc.selectedWorkspace, + 'workspace_id': event.workspaceId ?? ProfileBloc.selectedWorkspace, 'message_id': message.id, 'thread_id': message.threadId, 'original_str': event.content, diff --git a/lib/blocs/single_message_bloc/single_message_event.dart b/lib/blocs/single_message_bloc/single_message_event.dart index da553945..36706aab 100644 --- a/lib/blocs/single_message_bloc/single_message_event.dart +++ b/lib/blocs/single_message_bloc/single_message_event.dart @@ -6,8 +6,9 @@ abstract class SingleMessageEvent extends Equatable { class UpdateContent extends SingleMessageEvent { final String content; + final String workspaceId; - const UpdateContent(this.content); + const UpdateContent({this.content, this.workspaceId}); @override List get props => [content]; diff --git a/lib/blocs/threads_bloc/threads_bloc.dart b/lib/blocs/threads_bloc/threads_bloc.dart index 0ac248cc..e10e2985 100644 --- a/lib/blocs/threads_bloc/threads_bloc.dart +++ b/lib/blocs/threads_bloc/threads_bloc.dart @@ -78,12 +78,14 @@ class ThreadsBloc @override Stream mapEventToState(MessagesEvent event) async* { if (event is LoadMessages) { + print("SELECTED THREAD: ${threadMessage.toJson()}"); if (threadMessage.responsesCount == 0) { repository.clean(); yield MessagesEmpty( threadMessage: threadMessage, parentChannel: parentChannel, ); + repository.logger.d("RETURNING FROM LOADING RESPONSES"); return; } yield MessagesLoading( @@ -116,12 +118,14 @@ class ThreadsBloc await Future.delayed(Duration(milliseconds: 100)); attempt -= 1; } - await repository.pullOne(_makeQueryParams(event), + final updateParent = await repository.pullOne(_makeQueryParams(event), addToItems: threadMessage != null ? threadMessage.id == event.threadId : false); + if (updateParent) { + _updateParentChannel(); + } _sortItems(); - _updateParentChannel(); messagesBloc.add(ModifyResponsesCount( channelId: event.channelId, threadId: event.threadId, @@ -147,7 +151,7 @@ class ThreadsBloc _sortItems(); yield messagesLoaded; } - _updateParentChannel(totalModifier: -1); + // _updateParentChannel(totalModifier: -1); } else if (event is SendMessage) { final String dummyId = _DUMMY_ID; final body = _makeQueryParams(event); @@ -184,7 +188,7 @@ class ThreadsBloc channelId: event.channelId, threadId: message.threadId, )); - _updateParentChannel(); + // _updateParentChannel(); }, ); this.repository.items.add(tempItem); @@ -245,7 +249,7 @@ class ThreadsBloc final channelId = messagesBloc.selectedChannel.id; messagesBloc.channelsBloc.add(ModifyMessageCount( channelId: channelId, - workspaceId: ProfileBloc.selectedWorkspace, + workspaceId: T == DirectsBloc ? "direct" : ProfileBloc.selectedWorkspace, companyId: ProfileBloc.selectedCompany, totalModifier: totalModifier, )); diff --git a/lib/pages/edit_channel.dart b/lib/pages/edit_channel.dart index a1f1c074..5de6b8f9 100644 --- a/lib/pages/edit_channel.dart +++ b/lib/pages/edit_channel.dart @@ -43,7 +43,7 @@ class _EditChannelState extends State { var _members = []; var _showHistoryForNew = true; - var _canSave = false; + var _canSave = true; var _emojiVisible = false; Channel _channel; @@ -341,6 +341,7 @@ class _EditChannelState extends State { hint: 'Channel name', controller: _nameController, focusNode: _nameFocusNode, + maxLength: 30, ), Divider( thickness: 0.5, diff --git a/lib/pages/initial_page.dart b/lib/pages/initial_page.dart index 8c40e3de..fe0b98c6 100644 --- a/lib/pages/initial_page.dart +++ b/lib/pages/initial_page.dart @@ -189,7 +189,10 @@ class _InitialPageState extends State with WidgetsBindingObserver { lazy: false, ), BlocProvider( - create: (_) => AddChannelBloc(state.initData.addChannel), + create: (_) => AddChannelBloc( + state.initData.addChannel, + state.initData.addDirect, + ), lazy: false, ), BlocProvider( diff --git a/lib/pages/messages_page.dart b/lib/pages/messages_page.dart index 801e7eb7..b3b4983e 100644 --- a/lib/pages/messages_page.dart +++ b/lib/pages/messages_page.dart @@ -93,44 +93,49 @@ class MessagesPage extends StatelessWidget { if (state.parentChannel is Channel) TextAvatar(state.parentChannel.icon), SizedBox(width: 12.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - state.parentChannel.name, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w600, - color: Color(0xff444444), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + state.parentChannel.name, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + color: Color(0xff444444), + ), + overflow: TextOverflow.ellipsis, + ), ), - overflow: TextOverflow.ellipsis, - ), - SizedBox(width: 6), - if ((state.parentChannel is Channel) && - (state.parentChannel as Channel).visibility != - null && - (state.parentChannel as Channel).visibility == - 'private') - Icon(Icons.lock_outline, - size: 15.0, color: Color(0xff444444)), - ], - ), - if (state.parentChannel is Channel) - Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text( - '${(state.parentChannel as Channel).membersCount != null && (state.parentChannel as Channel).membersCount > 0 ? (state.parentChannel as Channel).membersCount : 'No'} members', - style: TextStyle( - fontSize: 10.0, - fontWeight: FontWeight.w400, - color: Color(0xff92929C), + SizedBox(width: 6), + if ((state.parentChannel is Channel) && + (state.parentChannel as Channel).visibility != + null && + (state.parentChannel as Channel).visibility == + 'private') + Icon(Icons.lock_outline, + size: 15.0, color: Color(0xff444444)), + ], + ), + if (state.parentChannel is Channel) + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text( + '${(state.parentChannel as Channel).membersCount != null && (state.parentChannel as Channel).membersCount > 0 ? (state.parentChannel as Channel).membersCount : 'No'} members', + style: TextStyle( + fontSize: 10.0, + fontWeight: FontWeight.w400, + color: Color(0xff92929C), + ), ), ), - ), - ], + ], + ), ), + SizedBox(width: 15), ], ), ); diff --git a/lib/pages/server_configuration.dart b/lib/pages/server_configuration.dart index a7a7cd2e..cdaaaa75 100644 --- a/lib/pages/server_configuration.dart +++ b/lib/pages/server_configuration.dart @@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:twake/blocs/auth_bloc/auth_bloc.dart'; import 'package:twake/blocs/configuration_cubit/configuration_cubit.dart'; import 'package:twake/blocs/configuration_cubit/configuration_state.dart'; -import 'package:twake/services/api.dart'; +// import 'package:twake/services/api.dart'; import 'package:twake/utils/extensions.dart'; class ServerConfiguration extends StatefulWidget { @@ -53,164 +53,172 @@ class _ServerConfigurationState extends State { ), ), body: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(height: 20.0), - Image.asset('assets/images/server.png'), - SizedBox(height: 20.0), - Text( - 'Server connection\npreference', - textAlign: TextAlign.center, - maxLines: 2, - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.w900, - color: Colors.black, - ), - ), - SizedBox(height: 36.0), - Padding( - padding: EdgeInsets.only(left: 16, right: 36.0), - child: Text( - 'Before you can proceed, please, choose a default server connection', + child: GestureDetector( + onTap: () => FocusScope.of(context).requestFocus(FocusNode()), + behavior: HitTestBehavior.opaque, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 10.0), + Image.asset('assets/images/server.png'), + SizedBox(height: 15.0), + Text( + 'Server connection\npreference', + textAlign: TextAlign.center, + maxLines: 2, style: TextStyle( - fontSize: 15.0, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity(0.6), + fontSize: 20.0, + fontWeight: FontWeight.w900, + color: Colors.black, ), ), - ), - Padding( - padding: EdgeInsets.fromLTRB(14.0, 12.0, 14.0, 0), - child: BlocListener( - listener: (context, state) { - if (state is HostValidated) { - widget.onConfirm(); - } - if (state is HostInvalid) { - print('HOST INVALID'); - Scaffold.of(context).showSnackBar(SnackBar( - content: Text( - 'Invalid host', - style: TextStyle( - color: Colors.red, + SizedBox(height: 36.0), + Padding( + padding: EdgeInsets.only(left: 16, right: 36.0), + child: Text( + 'Before you can proceed, please, choose a default server connection', + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity(0.6), + ), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(14.0, 12.0, 14.0, 0), + child: BlocListener( + listener: (context, state) { + if (state is HostValidated) { + widget.onConfirm(); + } + if (state is HostInvalid) { + print('HOST INVALID'); + Scaffold.of(context).showSnackBar(SnackBar( + content: Text( + 'Invalid host', + style: TextStyle( + color: Colors.red, + ), ), - ), - duration: Duration(seconds: 2), - )); - } - }, - child: BlocConsumer( - listenWhen: (_, current) => - current is ConfigurationSaved || - current is ConfigurationError, - listener: (context, state) { - if (state is ConfigurationSaved) { - context.read().add(ValidateHost(_controller.text)); - } else if (state is ConfigurationError) { - Scaffold.of(context).showSnackBar(SnackBar( - content: Text( - state.message, - style: TextStyle( - color: Colors.red, + duration: Duration(seconds: 2), + )); + } + }, + child: BlocConsumer( + listenWhen: (_, current) => + current is ConfigurationSaved || + current is ConfigurationError, + listener: (context, state) { + if (state is ConfigurationSaved) { + context + .read() + .add(ValidateHost(_controller.text)); + } else if (state is ConfigurationError) { + Scaffold.of(context).showSnackBar(SnackBar( + content: Text( + state.message, + style: TextStyle( + color: Colors.red, + ), ), - ), - duration: Duration(seconds: 2), - )); - } - }, - buildWhen: (_, current) => current is ConfigurationLoaded, - builder: (context, state) { - if (state is ConfigurationLoaded) { - _controller.text = state.host; - print('Server host: ${state.host}'); + duration: Duration(seconds: 2), + )); + } + }, + buildWhen: (_, current) => current is ConfigurationLoaded, + builder: (context, state) { + if (state is ConfigurationLoaded) { + if (_controller.text.isReallyEmpty) { + _controller.text = state.host; + } + print('Server host: ${state.host}'); - return TextFormField( - key: _formKey, - validator: (value) => - value.isEmpty ? 'Address cannot be blank' : null, - controller: _controller, - keyboardType: TextInputType.emailAddress, - style: TextStyle( - fontSize: 17.0, - fontWeight: FontWeight.w400, - color: Colors.black, - ), - decoration: InputDecoration( - hintText: 'https://mobile.api.twake.app', - hintStyle: TextStyle( + return TextFormField( + key: _formKey, + validator: (value) => + value.isEmpty ? 'Address cannot be blank' : null, + controller: _controller, + onFieldSubmitted: (_) => _connect(), + style: TextStyle( fontSize: 17.0, fontWeight: FontWeight.w400, - color: Color(0xffc8c8c8), + color: Colors.black, ), - alignLabelWithHint: true, - fillColor: Color(0xfff4f4f4), - filled: true, - suffix: Container( - width: 30, - height: 25, - padding: EdgeInsets.only(left: 10), - child: IconButton( - padding: EdgeInsets.all(0), - onPressed: () => _controller.clear(), - iconSize: 15, - icon: Icon(CupertinoIcons.clear), + decoration: InputDecoration( + hintText: 'https://mobile.api.twake.app', + hintStyle: TextStyle( + fontSize: 17.0, + fontWeight: FontWeight.w400, + color: Color(0xffc8c8c8), ), - ), - border: UnderlineInputBorder( - borderRadius: BorderRadius.circular(10.0), - borderSide: BorderSide( - width: 0.0, - style: BorderStyle.none, + alignLabelWithHint: true, + fillColor: Color(0xfff4f4f4), + filled: true, + suffix: Container( + width: 30, + height: 25, + padding: EdgeInsets.only(left: 10), + child: IconButton( + padding: EdgeInsets.all(0), + onPressed: () => _controller.clear(), + iconSize: 15, + icon: Icon(CupertinoIcons.clear), + ), + ), + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide( + width: 0.0, + style: BorderStyle.none, + ), ), ), - ), - ); - } else { - return Center(child: CircularProgressIndicator()); - } - }), - ), - ), - Spacer(), - Padding( - padding: EdgeInsets.symmetric(horizontal: 40.0), - child: Text( - 'Tap “Connect” if you don’t know exactly what is this all about', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15.0, - fontWeight: FontWeight.normal, - color: Colors.black, + ); + } else { + return Center(child: CircularProgressIndicator()); + } + }), ), ), - ), - Padding( - padding: EdgeInsets.fromLTRB(24.0, 16.0, 24.0, 22.0), - child: TextButton( - onPressed: () => _connect(), - child: Container( - width: Size.infinite.width, - height: 50, - decoration: BoxDecoration( - color: Color(0xff3840f7), - borderRadius: BorderRadius.circular(14.0), + Spacer(), + Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Text( + 'Tap “Connect” if you don’t know exactly what is this all about', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.normal, + color: Colors.black, ), - alignment: Alignment.center, - child: Text( - 'Connect', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 17.0, - fontWeight: FontWeight.bold, - color: Colors.white, + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(24.0, 16.0, 24.0, 22.0), + child: TextButton( + onPressed: () => _connect(), + child: Container( + width: Size.infinite.width, + height: 50, + decoration: BoxDecoration( + color: Color(0xff3840f7), + borderRadius: BorderRadius.circular(14.0), + ), + alignment: Alignment.center, + child: Text( + 'Connect', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 17.0, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/repositories/add_channel_repository.dart b/lib/repositories/add_channel_repository.dart index 49a189cb..63459bac 100644 --- a/lib/repositories/add_channel_repository.dart +++ b/lib/repositories/add_channel_repository.dart @@ -117,4 +117,4 @@ class AddChannelRepository { String channelId = resp['id']; return channelId; } -} +} \ No newline at end of file diff --git a/lib/repositories/add_direct_repository.dart b/lib/repositories/add_direct_repository.dart new file mode 100644 index 00000000..600ec6eb --- /dev/null +++ b/lib/repositories/add_direct_repository.dart @@ -0,0 +1,56 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:logger/logger.dart'; +import 'package:twake/blocs/profile_bloc/profile_bloc.dart'; +import 'package:twake/services/api.dart'; +import 'package:twake/services/endpoints.dart'; + +part 'add_direct_repository.g.dart'; + +@JsonSerializable(explicitToJson: true) +class AddDirectRepository { + @JsonKey(required: true, name: 'company_id') + String companyId; + @JsonKey(required: true) + String member; + @JsonKey(required: true, name: 'workspace_id') + String workspaceId = 'direct'; + + @JsonKey(ignore: true) + static final _logger = Logger(); + @JsonKey(ignore: true) + static final _api = Api(); + + AddDirectRepository({ + this.companyId, + this.workspaceId, + this.member, + }); + + factory AddDirectRepository.fromJson(Map json) => + _$AddDirectRepositoryFromJson(json); + + Map toJson() => _$AddDirectRepositoryToJson(this); + + Future clear() async { + companyId = ''; + member = ''; + } + + Future create() async { + this.companyId = ProfileBloc.selectedCompany; + this.workspaceId = 'direct'; + + final body = this.toJson(); + _logger.d('Direct creation request body: $body'); + Map resp; + try { + resp = await _api.post(Endpoint.directs, body: body); + } catch (error) { + _logger.e('Error while trying to create a direct:\n${error.message}'); + return ''; + } + _logger.d('RESPONSE AFTER DIRECT CREATION: $resp'); + String directId = resp['id']; + return directId; + } +} diff --git a/lib/repositories/add_direct_repository.g.dart b/lib/repositories/add_direct_repository.g.dart new file mode 100644 index 00000000..98289d61 --- /dev/null +++ b/lib/repositories/add_direct_repository.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'add_direct_repository.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AddDirectRepository _$AddDirectRepositoryFromJson(Map json) { + $checkKeys(json, + requiredKeys: const ['company_id', 'member', 'workspace_id']); + return AddDirectRepository() + ..companyId = json['company_id'] as String + ..member = json['member'] as String + ..workspaceId = json['workspace_id'] as String; +} + +Map _$AddDirectRepositoryToJson( + AddDirectRepository instance) => + { + 'company_id': instance.companyId, + 'member': instance.member, + 'workspace_id': instance.workspaceId, + }; diff --git a/lib/repositories/collection_repository.dart b/lib/repositories/collection_repository.dart index 80c670f8..2ad34788 100644 --- a/lib/repositories/collection_repository.dart +++ b/lib/repositories/collection_repository.dart @@ -84,7 +84,10 @@ class CollectionRepository { return collection; } - void select(String itemId, {bool saveToStore: true}) { + void select(String itemId, + {bool saveToStore: true, + String apiEndpoint, + Map params}) { // logger.w('BEFORE SELECT $T ${selected.id}'); final item = items.firstWhere((i) => i.id == itemId); var oldSelected = selected; @@ -93,6 +96,11 @@ class CollectionRepository { assert(selected.id == item.id); if (saveToStore) saveOne(item); saveOne(oldSelected); + if (apiEndpoint != null && params != null) + _api.post( + apiEndpoint, + body: params, + ); // logger.w('AFTER SELECT $T ${selected.id}'); } diff --git a/lib/repositories/messages_repository.dart b/lib/repositories/messages_repository.dart index 1445ef73..f2502748 100644 --- a/lib/repositories/messages_repository.dart +++ b/lib/repositories/messages_repository.dart @@ -157,11 +157,14 @@ class MessagesRepository { if (m == null) return null; // print('BEFORE COUNT: ${m.responsesCount}\nID: $messageId'); final sqlT = 'SELECT count(id) as count FROM message'; - final res = (await _storage.customQuery(sqlT, filters: [ + var res; + res = (await _storage.customQuery(sqlT, filters: [ ['thread_id', '=', messageId] ]))[0]['count']; - m.responsesCount = res; - _storage.store(item: m.toJson(), type: StorageType.Message); + if (res != 0) { + m.responsesCount = res; + await _storage.store(item: m.toJson(), type: StorageType.Message); + } // print('RESPONSES COUNT: $res'); // final sql = 'UPDATE message SET responses_count = ' // '(SELECT count(id) FROM message WHERE thread_id = ?) WHERE id = ?'; @@ -180,16 +183,21 @@ class MessagesRepository { Map queryParams, { bool addToItems = true, }) async { - // logger.d('Pulling item Message from api...\nPARAMS: $queryParams'); + logger.d('Pulling item Message from api...\nPARAMS: $queryParams'); List resp = []; try { resp = (await _api.get(apiEndpoint, params: queryParams)); } on ApiError catch (error) { - logger.d('ERROR while loading more Message from api\n${error.message}'); + logger.e('ERROR while loading more Message from api\n${error.message}'); return false; } if (resp.isEmpty) return false; var item = Message.fromJson(resp[0]); + var isNew = true; + if (await getItemById(queryParams['message_id']) != null) { + logger.e("MESSAGE EXISTS"); + isNew = false; + } saveOne(item); if (addToItems) { final query = 'SELECT message.*, ' @@ -219,8 +227,8 @@ class MessagesRepository { this.items.add(message); } - // logger.d('Pulled item: ${item.toJson()}'); - return true; + logger.d('Pulled item: ${item.toJson()}'); + return isNew; } Future pushOne( @@ -251,9 +259,9 @@ class MessagesRepository { if (!forceFromDB) item = items.firstWhere((i) => i.id == id, orElse: () => null); if (item == null) { - // print('GETTING MESSAGE BY ID: $id'); + print('GETTING MESSAGE BY ID: $id'); var map = await _storage.load(type: StorageType.Message, key: id); - // print('MESSAGE: $map'); + print('MESSAGE: $map'); if (map == null) return null; item = Message.fromJson(map); } diff --git a/lib/repositories/profile_repository.dart b/lib/repositories/profile_repository.dart index 486ce931..5564a79c 100644 --- a/lib/repositories/profile_repository.dart +++ b/lib/repositories/profile_repository.dart @@ -40,6 +40,10 @@ class ProfileRepository extends JsonSerializable { String selectedCompanyId; @JsonKey(name: 'selected_workspace_id') String selectedWorkspaceId; + @JsonKey(ignore: true) + String selectedChannelId; + @JsonKey(ignore: true) + String selectedThreadId; // Pseudo constructor for loading profile from storage or api static Future load() async { diff --git a/lib/services/endpoints.dart b/lib/services/endpoints.dart index 0b804ccc..d3f1a647 100644 --- a/lib/services/endpoints.dart +++ b/lib/services/endpoints.dart @@ -19,6 +19,8 @@ class Endpoint { static const workspaceMembers = '/workspaces/members'; // API Endpoint for working with user's channels in a workspace static const channels = '/channels'; + // API Endpoint for marking the channel as read + static const channelsRead = '/channels/read'; // API Endpoint for working with the members of user's channels static const channelMembers = '/channels/members'; // API Endpoint for working with user's direct channels with other users diff --git a/lib/services/init.dart b/lib/services/init.dart index d3b6b298..d043abcd 100644 --- a/lib/services/init.dart +++ b/lib/services/init.dart @@ -5,10 +5,10 @@ import 'package:twake/models/company.dart'; import 'package:twake/models/direct.dart'; import 'package:twake/models/workspace.dart'; import 'package:twake/repositories/add_channel_repository.dart'; +import 'package:twake/repositories/add_direct_repository.dart'; import 'package:twake/repositories/add_workspace_repository.dart'; import 'package:twake/repositories/auth_repository.dart'; import 'package:twake/repositories/collection_repository.dart'; -import 'package:twake/repositories/configuration_repository.dart'; import 'package:twake/repositories/edit_channel_repository.dart'; import 'package:twake/repositories/fields_repository.dart'; import 'package:twake/repositories/member_repository.dart'; @@ -46,6 +46,7 @@ Future initMain() async { final profile = await ProfileRepository.load(); 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: {}); @@ -119,6 +120,7 @@ Future initMain() async { threads: threads, sheet: sheet, addChannel: addChannel, + addDirect: addDirect, editChannel: editChannel, channelMembers: channelMembers, addWorkspace: addWorkspace, @@ -139,6 +141,7 @@ class InitData { final MessagesRepository threads; final SheetRepository sheet; final AddChannelRepository addChannel; + final AddDirectRepository addDirect; final EditChannelRepository editChannel; final AddWorkspaceRepository addWorkspace; final DraftRepository draft; @@ -156,6 +159,7 @@ class InitData { this.threads, this.sheet, this.addChannel, + this.addDirect, this.editChannel, this.addWorkspace, this.draft, diff --git a/lib/services/notifications.dart b/lib/services/notifications.dart index 48fdc751..21390622 100644 --- a/lib/services/notifications.dart +++ b/lib/services/notifications.dart @@ -5,6 +5,7 @@ import 'package:firebase_messaging/firebase_messaging.dart'; // import 'package:twake/blocs/profile_bloc/profile_bloc.dart'; import 'package:twake/models/notification.dart'; import 'package:twake/services/service_bundle.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; class Notifications { final logger = Logger(); @@ -12,13 +13,43 @@ class Notifications { final Function(MessageNotification) onMessageCallback; final Function(MessageNotification) onResumeCallback; final Function(MessageNotification) onLaunchCallback; + final bool Function(MessageNotification) shouldNotify; FirebaseMessaging _fcm = FirebaseMessaging(); + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); // 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), + // ), + // ); + // }, + // ) + // ], + // ), + // ); +// } Notifications({ this.onMessageCallback, this.onResumeCallback, this.onLaunchCallback, + this.shouldNotify, }) { if (Platform.isAndroid) this.platform = Target.Android; @@ -34,6 +65,29 @@ class Notifications { onResume: onResume, onLaunch: onLaunch, ); + + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('logo_blue'); + final IOSInitializationSettings initializationSettingsIOS = + IOSInitializationSettings(); + final InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); + flutterLocalNotificationsPlugin.initialize(initializationSettings, + onSelectNotification: (payload) async { + print("PAYLOAD FROM NOTIFY: $payload"); + final map = jsonDecode(payload); + print("NOTIFY CLICK: $map"); + try { + final notification = MessageNotification.fromJson(map); + print("NOTIFICATION PARSED: $notification"); + onMessageCallback(notification); + } catch (e) { + logger.wtf('ERROR PARSING NOTIFY: $e'); + } + }); } // Future checkWhatsNew(String workspaceId) async { @@ -50,30 +104,91 @@ class Notifications { // } Future onMessage(Map message) async { - // logger.d('GOT MESSAGE FROM FIREBASE: $message'); - // final notification = messageParse(message); - // onMessageCallback(notification); + logger.d('GOT MESSAGE FROM FIREBASE: $message'); + final notification = messageParse(message); + if (!shouldNotify(notification)) return; + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'some-random-text', 'Twake', 'Twake Mobile App', + importance: Importance.max, + priority: Priority.high, + showWhen: false); + const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 0, + _getTitle(message), + _getBody(message), + platformChannelSpecifics, + payload: _getPayload(message), + ); + } + + MessageNotification messageParse(Map message) { + // logger.d("ok, that's what we have:\n$data"); + final data = jsonDecode(_getPayload(message)); + MessageNotification notification = MessageNotification.fromJson(data); + return notification; } - NotificationData messageParse(Map message) { - Map data; + String _getBody(Map message) { + var data; switch (platform) { case Target.Android: // logger.d('Android notification received\n$message'); - data = jsonDecode(message['data']['notification_data']); + data = message['notification']['body']; break; case Target.IOS: // logger.d('iOS notification received\n$message'); - data = message['data']; + data = message['apps']['alert']['body']; break; case Target.Linux: case Target.MacOS: case Target.Windows: throw 'Desktop is not supported'; } - // logger.d("ok, that's what we have:\n$data"); - MessageNotification notification = MessageNotification.fromJson(data); - return notification; + 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['apps']['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']; + break; + case Target.IOS: + // logger.d('iOS notification received\n$message'); + data = message['notification_data']; + break; + case Target.Linux: + case Target.MacOS: + case Target.Windows: + throw 'Desktop is not supported'; + } + if (data.runtimeType == Map) { + return jsonEncode(data); + } + return data; } Future onResume(Map message) async { diff --git a/lib/utils/dateformatter.dart b/lib/utils/dateformatter.dart index bb533ac5..f38907aa 100644 --- a/lib/utils/dateformatter.dart +++ b/lib/utils/dateformatter.dart @@ -4,7 +4,7 @@ class DateFormatter { /// Function to get a verbose representation of timestamp, /// like [just now] or [today] static String getVerboseDateTime(int timestamp) { - if (timestamp == null) return ''; + if (timestamp == null || timestamp == 0) return ''; final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); final localDT = dateTime.toLocal(); // if timestamp is less than a minute old return 'Now' diff --git a/lib/widgets/auth/auth_form.dart b/lib/widgets/auth/auth_form.dart index d607f42b..9a179567 100644 --- a/lib/widgets/auth/auth_form.dart +++ b/lib/widgets/auth/auth_form.dart @@ -4,7 +4,7 @@ import 'package:twake/blocs/auth_bloc/auth_bloc.dart'; import 'package:twake/blocs/connection_bloc/connection_bloc.dart' as cb; import 'package:twake/config/styles_config.dart'; import 'package:twake/config/dimensions_config.dart' show Dim; -import 'package:twake/utils/navigation.dart'; +// import 'package:twake/utils/navigation.dart'; class AuthForm extends StatefulWidget { final Function onConfigurationOpen; @@ -44,6 +44,11 @@ class _AuthFormState extends State { super.initState(); _usernameController.addListener(onUsernameSaved); _passwordController.addListener(onPasswordSaved); + final state = BlocProvider.of(context).state; + if (state is Unauthenticated) { + _usernameController.text = state.username; + _passwordController.text = state.username; + } } @override diff --git a/lib/widgets/channel/channel_tile.dart b/lib/widgets/channel/channel_tile.dart index 1b397da2..231ca337 100644 --- a/lib/widgets/channel/channel_tile.dart +++ b/lib/widgets/channel/channel_tile.dart @@ -40,16 +40,18 @@ class ChannelTile extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - channel.name, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, - style: TextStyle( - fontSize: 16.0, - fontWeight: channel.hasUnread == 1 - ? FontWeight.w900 - : FontWeight.w400, - color: Color(0xff444444), + Expanded( + child: Text( + channel.name, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 16.0, + fontWeight: channel.hasUnread == 1 + ? FontWeight.w900 + : FontWeight.w400, + color: Color(0xff444444), + ), ), ), SizedBox(width: 6), @@ -77,6 +79,7 @@ class ChannelTile extends StatelessWidget { ], ), ), + SizedBox(width: 15), Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/widgets/channel/direct_tile.dart b/lib/widgets/channel/direct_tile.dart index 85a8e87a..7565a907 100644 --- a/lib/widgets/channel/direct_tile.dart +++ b/lib/widgets/channel/direct_tile.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:twake/blocs/channels_bloc/channels_bloc.dart'; -import 'package:twake/blocs/directs_bloc/directs_bloc.dart'; +// import 'package:twake/blocs/channels_bloc/channels_bloc.dart'; +// import 'package:twake/blocs/directs_bloc/directs_bloc.dart'; import 'package:twake/blocs/draft_bloc/draft_bloc.dart'; import 'package:twake/config/dimensions_config.dart' show Dim; import 'package:twake/models/direct.dart'; -import 'package:twake/pages/messages_page.dart'; +// import 'package:twake/pages/messages_page.dart'; import 'package:twake/repositories/draft_repository.dart'; import 'package:twake/utils/dateformatter.dart'; import 'package:twake/utils/navigation.dart'; @@ -20,7 +20,6 @@ class DirectTile extends StatelessWidget { Widget build(BuildContext context) { return InkWell( onTap: () { - var draftType = DraftType.direct; final id = direct.id; // Load draft from local storage @@ -47,7 +46,9 @@ class DirectTile extends StatelessWidget { textAlign: TextAlign.start, style: TextStyle( fontSize: 16.0, - fontWeight: FontWeight.w600, + fontWeight: direct.hasUnread == 1 + ? FontWeight.w900 + : FontWeight.w400, color: Color(0xff444444), ), ), diff --git a/lib/widgets/common/reaction.dart b/lib/widgets/common/reaction.dart index d88f9276..6eb2d6ec 100644 --- a/lib/widgets/common/reaction.dart +++ b/lib/widgets/common/reaction.dart @@ -7,14 +7,18 @@ import 'package:twake/config/styles_config.dart'; class Reaction extends StatelessWidget { final String reaction; final int count; - Reaction(this.reaction, this.count); + final String workspaceId; + Reaction(this.reaction, this.count, [this.workspaceId]); @override Widget build(BuildContext context) { return InkWell( onTap: () { BlocProvider.of(context).add( - UpdateReaction(emojiCode: reaction), + UpdateReaction( + emojiCode: reaction, + workspaceId: workspaceId, + ), ); }, child: FittedBox( diff --git a/lib/widgets/drawer/twake_drawer.dart b/lib/widgets/drawer/twake_drawer.dart index 1cc3751a..5ed35b5e 100644 --- a/lib/widgets/drawer/twake_drawer.dart +++ b/lib/widgets/drawer/twake_drawer.dart @@ -107,32 +107,36 @@ class _TwakeDrawerState extends State { height: 30, ), SizedBox(width: 15), - Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox(height: 12), - Text( - state.workspaces[i].name, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w600, - color: Color(0xff444444), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox(height: 12), + Text( + state.workspaces[i].name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + color: Color(0xff444444), + ), ), - ), - SizedBox(height: 4), - Text( - '${state.workspaces[i].totalMembers} members', - style: TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.w400, - color: Color(0xff444444), + SizedBox(height: 4), + Text( + '${state.workspaces[i].totalMembers} members', + style: TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w400, + color: Color(0xff444444), + ), ), - ), - SizedBox(height: 12), - ], + SizedBox(height: 12), + ], + ), ), ], ), diff --git a/lib/widgets/message/message_tile.dart b/lib/widgets/message/message_tile.dart index 8769759b..1c5741bc 100644 --- a/lib/widgets/message/message_tile.dart +++ b/lib/widgets/message/message_tile.dart @@ -3,6 +3,7 @@ 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'; import 'package:twake/blocs/message_edit_bloc/message_edit_bloc.dart'; import 'package:twake/blocs/messages_bloc/messages_bloc.dart'; @@ -118,7 +119,10 @@ class _MessageTileState onMessageEditComplete: (text) { // smbloc get's closed if // listview disposes of message tile - smbloc.add(UpdateContent(text)); + smbloc.add(UpdateContent( + content: text, + workspaceId: + T == DirectsBloc ? 'direct' : null)); mebloc.add(CancelMessageEdit()); FocusManager.instance.primaryFocus.unfocus(); }), @@ -236,6 +240,7 @@ class _MessageTileState return Reaction( r, messageState.reactions[r]['count'], + T == DirectsBloc ? 'direct' : null, ); }), if (messageState.responsesCount > 0 && diff --git a/lib/widgets/sheets/add/channel_info_form.dart b/lib/widgets/sheets/add/channel_info_form.dart index 3465dd95..fcf29776 100644 --- a/lib/widgets/sheets/add/channel_info_form.dart +++ b/lib/widgets/sheets/add/channel_info_form.dart @@ -221,6 +221,7 @@ class _ChannelInfoFormState extends State { hint: 'Channel name', controller: _channelNameController, focusNode: _channelNameFocusNode, + maxLength: 30, ), ), ], diff --git a/lib/widgets/sheets/add/participants_list.dart b/lib/widgets/sheets/add/participants_list.dart index a7a8e3bf..798cb72b 100644 --- a/lib/widgets/sheets/add/participants_list.dart +++ b/lib/widgets/sheets/add/participants_list.dart @@ -108,12 +108,10 @@ class _ParticipantsListState extends State { void _createDirect(List participantsIds) { context.read() - ..add(Update( - name: '', - type: ChannelType.direct, - participants: participantsIds, + ..add(UpdateDirect( + member: participantsIds.first, )) - ..add(Create()); + ..add(CreateDirect()); FocusScope.of(context).requestFocus(FocusNode()); } @@ -127,9 +125,11 @@ class _ParticipantsListState extends State { }, child: BlocConsumer( listener: (context, state) { - if (state is Created) { + if (state is Created || state is DirectCreated) { // Reload channels - context.read().add(ReloadChannels(forceFromApi: true)); + context + .read() + .add(ReloadChannels(forceFromApi: true)); // Reload directs context.read().add(ReloadChannels(forceFromApi: true)); // Close sheet @@ -138,7 +138,7 @@ class _ParticipantsListState extends State { context.read().add(Update(participants: [])); _controller.clear(); // Redirect user to created direct - if (_isDirect) { + if (state is DirectCreated) { String channelId = state.id; openDirect(context, channelId); } @@ -157,6 +157,7 @@ class _ParticipantsListState extends State { }, buildWhen: (_, current) { return (current is Updated || + current is DirectUpdated || current is Creation || current is StageUpdated); }, @@ -205,7 +206,8 @@ class _ParticipantsListState extends State { // print('-------------------------------'); } return BlocBuilder( - buildWhen: (previous, current) => current is Updated, + buildWhen: (previous, current) => + current is Updated, builder: (context, state) { var selectedIds = []; var selectedUsers = []; @@ -222,7 +224,6 @@ class _ParticipantsListState extends State { users.excludeUsers(selectedUsers); } } - // print('Selected UsERS: ${selectedUsers.map((e) => e.username)}'); return ListView.builder( shrinkWrap: true, diff --git a/lib/widgets/sheets/add/workspace_info_form.dart b/lib/widgets/sheets/add/workspace_info_form.dart index a3989ffd..66f788d6 100644 --- a/lib/widgets/sheets/add/workspace_info_form.dart +++ b/lib/widgets/sheets/add/workspace_info_form.dart @@ -152,6 +152,7 @@ class _WorkspaceInfoFormState extends State { hint: 'Workspace name', controller: _workspaceNameController, focusNode: _workspaceNameFocusNode, + maxLength: 30, ), ), ), diff --git a/lib/widgets/sheets/edit/member_management.dart b/lib/widgets/sheets/edit/member_management.dart index 3071c21d..3aeb1ed4 100644 --- a/lib/widgets/sheets/edit/member_management.dart +++ b/lib/widgets/sheets/edit/member_management.dart @@ -62,7 +62,7 @@ class _MemberManagementState extends State { _channelId = state.channelId; _members = state.members; - print(_members.map((e) => e.email)); + // print(_members.map((e) => e.email)); _himself ??= _members.firstWhere( (m) => m.id == ProfileBloc.userId, @@ -71,9 +71,9 @@ class _MemberManagementState extends State { _members.removeWhere((m) => m.userId == _himself.userId); _members.insert(0, _himself); - print(_members.map((e) => e.email)); - print(_members.map((e) => e.userId)); - print(ProfileBloc.userId); + // print(_members.map((e) => e.email)); + // print(_members.map((e) => e.userId)); + // print(ProfileBloc.userId); } return Column( children: [ diff --git a/lib/widgets/sheets/sheet_text_field.dart b/lib/widgets/sheets/sheet_text_field.dart index 5d5325ff..934c545d 100644 --- a/lib/widgets/sheets/sheet_text_field.dart +++ b/lib/widgets/sheets/sheet_text_field.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class SheetTextField extends StatefulWidget { final String hint; @@ -7,6 +8,7 @@ class SheetTextField extends StatefulWidget { final Function trailingAction; final TextEditingController controller; final FocusNode focusNode; + final int maxLength; final String Function(String) validator; const SheetTextField({ @@ -16,6 +18,7 @@ class SheetTextField extends StatefulWidget { this.leadingAction, this.trailingAction, this.validator, + this.maxLength, }); @override @@ -29,6 +32,9 @@ class _SheetTextFieldState extends State { padding: const EdgeInsets.only(left: 14.0, right: 7), color: Colors.white, child: TextFormField( + inputFormatters:[ + LengthLimitingTextInputFormatter(widget.maxLength), + ], validator: widget.validator, controller: widget.controller, focusNode: widget.focusNode, diff --git a/pubspec.yaml b/pubspec.yaml index adf44b85..e41acdf9 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 +version: 2.2.9+1 environment: sdk: ">=2.7.0 <3.0.0" @@ -56,6 +56,8 @@ dependencies: flutter_markdown: git: https://github.com/bobs4462/flutter_markdown.git + flutter_local_notifications: ^4.0.1+1 + # The following adds the Cupertino Icons font to your application.