From 1c6c343b4a73d52011fa244ef26d4752eb4ad3b7 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sat, 16 Oct 2021 12:43:40 +0200 Subject: [PATCH 01/25] WIP: Migrate FilePicker to a separate file --- lib/utils/mobileFilePicker.dart | 64 +++++++++++++++++++++++++++++++++ lib/widgets/sendMessage.dart | 59 ++++-------------------------- 2 files changed, 71 insertions(+), 52 deletions(-) create mode 100644 lib/utils/mobileFilePicker.dart diff --git a/lib/utils/mobileFilePicker.dart b/lib/utils/mobileFilePicker.dart new file mode 100644 index 0000000..05bfb5e --- /dev/null +++ b/lib/utils/mobileFilePicker.dart @@ -0,0 +1,64 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:papercups_flutter/models/models.dart'; +import 'package:papercups_flutter/utils/uploadFile.dart'; +import 'package:papercups_flutter/widgets/alert.dart'; + +final mobileFilePicker = ( + {required FileType type, + required BuildContext context, + required widget, + required Function onUploadSuccess}) async { + try { + final _paths = (await FilePicker.platform.pickFiles( + type: type, + )) + ?.files; + if (_paths != null && _paths.first.path != null) { + var context; + Alert.show( + "Uploading...", + context, + textStyle: Theme.of(context).textTheme.bodyText2, + backgroundColor: Theme.of(context).bottomAppBarColor, + gravity: Alert.bottom, + duration: Alert.lengthLong, + ); + List attachments = await uploadFile( + widget.props, + filePath: _paths.first.path, + onUploadProgress: (sentBytes, totalBytes) { + Alert.show( + "${(sentBytes * 100 / totalBytes).toStringAsFixed(2)}% uploaded", + context, + textStyle: Theme.of(context).textTheme.bodyText2, + backgroundColor: Theme.of(context).bottomAppBarColor, + gravity: Alert.bottom, + duration: Alert.lengthLong, + ); + }, + ); + + onUploadSuccess(attachments); + } + } on PlatformException catch (_) { + Alert.show( + "Failed to upload attachment", + context, + textStyle: Theme.of(context).textTheme.bodyText2, + backgroundColor: Theme.of(context).bottomAppBarColor, + gravity: Alert.bottom, + duration: Alert.lengthLong, + ); + } catch (_) { + Alert.show( + "Failed to upload attachment", + context, + textStyle: Theme.of(context).textTheme.bodyText2, + backgroundColor: Theme.of(context).bottomAppBarColor, + gravity: Alert.bottom, + duration: Alert.lengthLong, + ); + } +}; diff --git a/lib/widgets/sendMessage.dart b/lib/widgets/sendMessage.dart index 00cc0de..39affbb 100644 --- a/lib/widgets/sendMessage.dart +++ b/lib/widgets/sendMessage.dart @@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:papercups_flutter/utils/mobileFilePicker.dart'; import 'package:papercups_flutter/utils/uploadFile.dart'; import '../models/models.dart'; import '../utils/utils.dart'; @@ -171,58 +172,12 @@ class _SendMessageState extends State { size: 18, ), ), - onSelected: (type) async { - try { - final _paths = (await FilePicker.platform.pickFiles( - type: type, - )) - ?.files; - if (_paths != null && _paths.first.path != null) { - Alert.show( - "Uploading...", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); - List attachments = await uploadFile( - widget.props, - filePath: _paths.first.path, - onUploadProgress: (sentBytes, totalBytes) { - Alert.show( - "${(sentBytes * 100 / totalBytes).toStringAsFixed(2)}% uploaded", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); - }, - ); - - _onUploadSuccess(attachments); - } - } on PlatformException catch (_) { - Alert.show( - "Failed to upload attachment", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); - } catch (_) { - Alert.show( - "Failed to upload attachment", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); - } - }, + onSelected: (type) => mobileFilePicker( + context: context, + onUploadSuccess: _onUploadSuccess, + type: type, + widget: widget, + ), itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: FileType.any, From 30577d463b265b6bbff1764cf22be28480421c0f Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sat, 16 Oct 2021 23:50:13 +0200 Subject: [PATCH 02/25] Reorganize utils folder --- .../getConversationDetails.dart | 2 +- .../{ => apiInteraction}/getCustomerDetails.dart | 16 +++++++++++----- .../getCustomerDetailsFromMetadata.dart | 2 +- .../{ => apiInteraction}/getCustomerHistory.dart | 6 +++--- .../getPastCustomerMessages.dart | 12 ++++++++---- .../{ => apiInteraction}/updateUserMetadata.dart | 6 +++--- .../{ => fileInteraction}/downloadFile.dart | 0 .../{ => fileInteraction}/mobileFilePicker.dart | 2 +- lib/utils/{ => fileInteraction}/uploadFile.dart | 2 +- lib/utils/{ => socket}/intitChannels.dart | 4 ++-- lib/utils/{ => socket}/joinConversation.dart | 6 +++--- lib/utils/utils.dart | 14 +++++++------- lib/widgets/chat.dart | 2 +- lib/widgets/sendMessage.dart | 9 ++++----- test/utils_test.dart | 2 +- 15 files changed, 47 insertions(+), 38 deletions(-) rename lib/utils/{ => apiInteraction}/getConversationDetails.dart (97%) rename lib/utils/{ => apiInteraction}/getCustomerDetails.dart (81%) rename lib/utils/{ => apiInteraction}/getCustomerDetailsFromMetadata.dart (97%) rename lib/utils/{ => apiInteraction}/getCustomerHistory.dart (95%) rename lib/utils/{ => apiInteraction}/getPastCustomerMessages.dart (88%) rename lib/utils/{ => apiInteraction}/updateUserMetadata.dart (93%) rename lib/utils/{ => fileInteraction}/downloadFile.dart (100%) rename lib/utils/{ => fileInteraction}/mobileFilePicker.dart (96%) rename lib/utils/{ => fileInteraction}/uploadFile.dart (98%) rename lib/utils/{ => socket}/intitChannels.dart (89%) rename lib/utils/{ => socket}/joinConversation.dart (97%) diff --git a/lib/utils/getConversationDetails.dart b/lib/utils/apiInteraction/getConversationDetails.dart similarity index 97% rename from lib/utils/getConversationDetails.dart rename to lib/utils/apiInteraction/getConversationDetails.dart index 9f960c8..dd36005 100644 --- a/lib/utils/getConversationDetails.dart +++ b/lib/utils/apiInteraction/getConversationDetails.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:http/http.dart'; -import '../models/models.dart'; +import '../../models/models.dart'; /// This function will get the conversation details that we need in order to join the room. /// The most important detail is the ID, and this will return a **new** conversation. diff --git a/lib/utils/getCustomerDetails.dart b/lib/utils/apiInteraction/getCustomerDetails.dart similarity index 81% rename from lib/utils/getCustomerDetails.dart rename to lib/utils/apiInteraction/getCustomerDetails.dart index 8694a2b..fbcba26 100644 --- a/lib/utils/getCustomerDetails.dart +++ b/lib/utils/apiInteraction/getCustomerDetails.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:http/http.dart'; -import '../models/models.dart'; -import 'utils.dart'; +import '../../models/models.dart'; +import '../utils.dart'; Future getCustomerDetails( Props p, @@ -18,7 +18,9 @@ Future getCustomerDetails( } try { var timeNow = DateTime.now().toUtc().toIso8601String(); - var metadata = p.customer != null && p.customer!.otherMetadata != null ? p.customer!.otherMetadata! : {}; + var metadata = p.customer != null && p.customer!.otherMetadata != null + ? p.customer!.otherMetadata! + : {}; var jsonString = jsonEncode( { "customer": { @@ -41,10 +43,14 @@ Future getCustomerDetails( ); var data = jsonDecode(res.body)["data"]; c = PapercupsCustomer( - createdAt: data["created_at"] != null ? parseDateFromUTC(data["created_at"]) : null, + createdAt: data["created_at"] != null + ? parseDateFromUTC(data["created_at"]) + : null, email: data["email"], externalId: data["external_id"], - firstSeen: data["first_seen"] != null ? parseDateFromUTC(data["first_seen"]) : null, + firstSeen: data["first_seen"] != null + ? parseDateFromUTC(data["first_seen"]) + : null, id: data["id"], lastSeenAt: data["last_seen_at"] != null ? parseDateFromUTC(data["last_seen_at"]) diff --git a/lib/utils/getCustomerDetailsFromMetadata.dart b/lib/utils/apiInteraction/getCustomerDetailsFromMetadata.dart similarity index 97% rename from lib/utils/getCustomerDetailsFromMetadata.dart rename to lib/utils/apiInteraction/getCustomerDetailsFromMetadata.dart index 729bf1e..7034f9c 100644 --- a/lib/utils/getCustomerDetailsFromMetadata.dart +++ b/lib/utils/apiInteraction/getCustomerDetailsFromMetadata.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:http/http.dart'; -import '../models/models.dart'; +import '../../models/models.dart'; /// This funtction is used to get the customer details from Papercups. /// This is the function responsible for finding the Customer's ID. diff --git a/lib/utils/getCustomerHistory.dart b/lib/utils/apiInteraction/getCustomerHistory.dart similarity index 95% rename from lib/utils/getCustomerHistory.dart rename to lib/utils/apiInteraction/getCustomerHistory.dart index 74fdcbf..9306ad8 100644 --- a/lib/utils/getCustomerHistory.dart +++ b/lib/utils/apiInteraction/getCustomerHistory.dart @@ -1,14 +1,14 @@ //Imports import 'updateUserMetadata.dart'; -import '../models/models.dart'; -import '../papercups_flutter.dart'; +import '../../models/models.dart'; +import '../../papercups_flutter.dart'; import 'package:phoenix_socket/phoenix_socket.dart'; import 'dart:async'; import 'getCustomerDetailsFromMetadata.dart'; import 'getPastCustomerMessages.dart'; -import 'joinConversation.dart'; +import '../socket/joinConversation.dart'; /// This function is used to get the history. /// It also initializes the necessary funtions if the customer is known. diff --git a/lib/utils/getPastCustomerMessages.dart b/lib/utils/apiInteraction/getPastCustomerMessages.dart similarity index 88% rename from lib/utils/getPastCustomerMessages.dart rename to lib/utils/apiInteraction/getPastCustomerMessages.dart index 9b9796b..90ce021 100644 --- a/lib/utils/getPastCustomerMessages.dart +++ b/lib/utils/apiInteraction/getPastCustomerMessages.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'package:http/http.dart'; -import '../models/models.dart'; -import 'utils.dart'; +import '../../models/models.dart'; +import '../utils.dart'; /// This function is used to get the past messages from the customer. Future> getPastCustomerMessages( @@ -55,8 +55,12 @@ Future> getPastCustomerMessages( email: val["user"]["email"], id: val["user"]["id"], role: val["user"]["role"], - fullName: (val["user"]["full_name"] != null) ? val["user"]["full_name"] : null, - profilePhotoUrl: (val["user"]["profile_photo_url"] != null) ? val["user"]["profile_photo_url"] : null, + fullName: (val["user"]["full_name"] != null) + ? val["user"]["full_name"] + : null, + profilePhotoUrl: (val["user"]["profile_photo_url"] != null) + ? val["user"]["profile_photo_url"] + : null, ) : null, ), diff --git a/lib/utils/updateUserMetadata.dart b/lib/utils/apiInteraction/updateUserMetadata.dart similarity index 93% rename from lib/utils/updateUserMetadata.dart rename to lib/utils/apiInteraction/updateUserMetadata.dart index 778f840..8bc1fe5 100644 --- a/lib/utils/updateUserMetadata.dart +++ b/lib/utils/apiInteraction/updateUserMetadata.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'package:http/http.dart'; -import '../papercups_flutter.dart'; +import '../../papercups_flutter.dart'; -import '../models/models.dart'; -import 'utils.dart'; +import '../../models/models.dart'; +import '../utils.dart'; /// This function is used to update customer details on the Papercups server. Future updateUserMetadata( diff --git a/lib/utils/downloadFile.dart b/lib/utils/fileInteraction/downloadFile.dart similarity index 100% rename from lib/utils/downloadFile.dart rename to lib/utils/fileInteraction/downloadFile.dart diff --git a/lib/utils/mobileFilePicker.dart b/lib/utils/fileInteraction/mobileFilePicker.dart similarity index 96% rename from lib/utils/mobileFilePicker.dart rename to lib/utils/fileInteraction/mobileFilePicker.dart index 05bfb5e..5720012 100644 --- a/lib/utils/mobileFilePicker.dart +++ b/lib/utils/fileInteraction/mobileFilePicker.dart @@ -2,7 +2,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:papercups_flutter/models/models.dart'; -import 'package:papercups_flutter/utils/uploadFile.dart'; +import 'package:papercups_flutter/utils/fileInteraction/uploadFile.dart'; import 'package:papercups_flutter/widgets/alert.dart'; final mobileFilePicker = ( diff --git a/lib/utils/uploadFile.dart b/lib/utils/fileInteraction/uploadFile.dart similarity index 98% rename from lib/utils/uploadFile.dart rename to lib/utils/fileInteraction/uploadFile.dart index 1c0850e..bbbc558 100644 --- a/lib/utils/uploadFile.dart +++ b/lib/utils/fileInteraction/uploadFile.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:http/http.dart'; -import '../models/models.dart'; +import '../../models/models.dart'; typedef void OnUploadProgressCallback(int sentBytes, int totalBytes); diff --git a/lib/utils/intitChannels.dart b/lib/utils/socket/intitChannels.dart similarity index 89% rename from lib/utils/intitChannels.dart rename to lib/utils/socket/intitChannels.dart index 5308393..2603836 100644 --- a/lib/utils/intitChannels.dart +++ b/lib/utils/socket/intitChannels.dart @@ -1,6 +1,6 @@ // Imports -import '../models/models.dart'; -import '../papercups_flutter.dart'; +import '../../models/models.dart'; +import '../../papercups_flutter.dart'; import 'package:phoenix_socket/phoenix_socket.dart'; /// This function creates the necessary channels, sockets and rooms for papercups to communicate. diff --git a/lib/utils/joinConversation.dart b/lib/utils/socket/joinConversation.dart similarity index 97% rename from lib/utils/joinConversation.dart rename to lib/utils/socket/joinConversation.dart index dac49b2..c404599 100644 --- a/lib/utils/joinConversation.dart +++ b/lib/utils/socket/joinConversation.dart @@ -1,8 +1,8 @@ -import '../models/models.dart'; +import '../../models/models.dart'; import 'package:phoenix_socket/phoenix_socket.dart'; -import 'utils.dart'; +import '../utils.dart'; -import '../models/message.dart'; +import '../../models/message.dart'; /// This function will join the channel and listen to new messages. PhoenixChannel? joinConversationAndListen({ diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 15eb81e..81269f5 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,10 +1,10 @@ /// Exports for the utilities, new utils should be added here and this is the file that should be imported instead of the specific util file. export 'colorMod.dart'; -export 'getConversationDetails.dart'; -export 'getCustomerDetails.dart'; -export 'getCustomerDetailsFromMetadata.dart'; -export 'getCustomerHistory.dart'; -export 'getPastCustomerMessages.dart'; -export 'intitChannels.dart'; -export 'joinConversation.dart'; +export 'apiInteraction/getConversationDetails.dart'; +export 'apiInteraction/getCustomerDetails.dart'; +export 'apiInteraction/getCustomerDetailsFromMetadata.dart'; +export 'apiInteraction/getCustomerHistory.dart'; +export 'apiInteraction/getPastCustomerMessages.dart'; +export 'socket/intitChannels.dart'; +export 'socket/joinConversation.dart'; export 'parseDateFromUTC.dart'; diff --git a/lib/widgets/chat.dart b/lib/widgets/chat.dart index 90aeaad..6721867 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:papercups_flutter/utils/downloadFile.dart'; +import 'package:papercups_flutter/utils/fileInteraction/downloadFile.dart'; import 'package:path_provider/path_provider.dart'; import 'package:timeago/timeago.dart' as timeago; import '../models/models.dart'; diff --git a/lib/widgets/sendMessage.dart b/lib/widgets/sendMessage.dart index 39affbb..0abbe7c 100644 --- a/lib/widgets/sendMessage.dart +++ b/lib/widgets/sendMessage.dart @@ -3,16 +3,15 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:papercups_flutter/utils/mobileFilePicker.dart'; -import 'package:papercups_flutter/utils/uploadFile.dart'; +import 'package:papercups_flutter/utils/fileInteraction/mobileFilePicker.dart'; +import 'package:papercups_flutter/utils/fileInteraction/uploadFile.dart'; import '../models/models.dart'; import '../utils/utils.dart'; import '../models/conversation.dart'; import '../models/customer.dart'; -import '../utils/getConversationDetails.dart'; -import '../utils/getCustomerDetails.dart'; +import '../utils/apiInteraction/getConversationDetails.dart'; +import '../utils/apiInteraction/getCustomerDetails.dart'; import 'package:phoenix_socket/phoenix_socket.dart'; import '../models/classes.dart'; diff --git a/test/utils_test.dart b/test/utils_test.dart index 8870d10..d8bf694 100644 --- a/test/utils_test.dart +++ b/test/utils_test.dart @@ -6,7 +6,7 @@ import 'package:mockito/mockito.dart'; import 'package:papercups_flutter/models/models.dart'; import 'package:http/http.dart' as http; -import 'package:papercups_flutter/utils/updateUserMetadata.dart'; +import 'package:papercups_flutter/utils/apiInteraction/updateUserMetadata.dart'; import 'mocks.mocks.dart'; From 3976ec462934620630ef580ed996bccea7eee6a5 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 00:01:12 +0200 Subject: [PATCH 03/25] Migrate and get ready for desktop file support --- .../fileInteraction/webDesktopFilePicker.dart | 41 +++++++++++++++++++ lib/widgets/sendMessage.dart | 39 ++++-------------- 2 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 lib/utils/fileInteraction/webDesktopFilePicker.dart diff --git a/lib/utils/fileInteraction/webDesktopFilePicker.dart b/lib/utils/fileInteraction/webDesktopFilePicker.dart new file mode 100644 index 0000000..4d78cb2 --- /dev/null +++ b/lib/utils/fileInteraction/webDesktopFilePicker.dart @@ -0,0 +1,41 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:papercups_flutter/models/attachment.dart'; +import 'package:papercups_flutter/utils/fileInteraction/uploadFile.dart'; +import 'package:papercups_flutter/widgets/alert.dart'; + +Future webDesktopFilePicker( + {required BuildContext context, + required Function onUploadSuccess, + required widget}) async { + try { + var picked = await FilePicker.platform.pickFiles(); + + if (picked != null && picked.files.first.bytes != null) { + Alert.show( + "Uploading...", + context, + textStyle: Theme.of(context).textTheme.bodyText2, + backgroundColor: Theme.of(context).bottomAppBarColor, + gravity: Alert.bottom, + duration: Alert.lengthLong, + ); + List attachments = await uploadFile( + widget.props, + fileBytes: picked.files.first.bytes, + fileName: picked.files.first.name, + ); + onUploadSuccess(attachments); + } + } on Exception catch (_) { + Alert.show( + "Failed to upload attachment", + context, + textStyle: Theme.of(context).textTheme.bodyText2, + backgroundColor: Theme.of(context).bottomAppBarColor, + gravity: Alert.bottom, + duration: Alert.lengthLong, + ); + } +} diff --git a/lib/widgets/sendMessage.dart b/lib/widgets/sendMessage.dart index 0abbe7c..ccfb68b 100644 --- a/lib/widgets/sendMessage.dart +++ b/lib/widgets/sendMessage.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:papercups_flutter/utils/fileInteraction/mobileFilePicker.dart'; import 'package:papercups_flutter/utils/fileInteraction/uploadFile.dart'; +import 'package:papercups_flutter/utils/fileInteraction/webDesktopFilePicker.dart'; import '../models/models.dart'; import '../utils/utils.dart'; import '../models/conversation.dart'; @@ -120,7 +121,7 @@ class _SendMessageState extends State { // TODO: Separate this widget // TODO: Internationalize alerts and popups Widget _getFilePicker() { - if (kIsWeb) { + if (kIsWeb || Platform.isWindows || Platform.isLinux || Platform.isMacOS) { return IconButton( splashRadius: 20, icon: Transform.rotate( @@ -130,37 +131,11 @@ class _SendMessageState extends State { size: 18, ), ), - onPressed: () async { - try { - var picked = await FilePicker.platform.pickFiles(); - - if (picked != null && picked.files.first.bytes != null) { - Alert.show( - "Uploading...", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); - List attachments = await uploadFile( - widget.props, - fileBytes: picked.files.first.bytes, - fileName: picked.files.first.name, - ); - _onUploadSuccess(attachments); - } - } on Exception catch (_) { - Alert.show( - "Failed to upload attachment", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); - } - }, + onPressed: () => webDesktopFilePicker( + context: context, + onUploadSuccess: _onUploadSuccess, + widget: widget, + ), ); } else if (Platform.isAndroid || Platform.isIOS) { return PopupMenuButton( From 58f50b4686e7e0087df69a5cdd92d406e1663cec Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 00:01:27 +0200 Subject: [PATCH 04/25] Upgrade file picker to 4.0.0 --- example/pubspec.lock | 8 ++++---- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index cff939f..ce17722 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -84,7 +84,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.2" file: dependency: transitive description: @@ -98,7 +98,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "4.1.4" flex_color_picker: dependency: "direct main" description: @@ -124,7 +124,7 @@ packages: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -438,4 +438,4 @@ packages: version: "0.2.0" sdks: dart: ">=2.12.0 <3.0.0" - flutter: ">=1.22.0" + flutter: ">=2.0.0" diff --git a/pubspec.lock b/pubspec.lock index add7064..c30ff4a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -182,7 +182,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.2" file: dependency: transitive description: @@ -196,7 +196,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "4.1.4" fixnum: dependency: transitive description: @@ -222,7 +222,7 @@ packages: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -646,4 +646,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.12.0 <3.0.0" - flutter: ">=1.22.0" + flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 01d9de0..87cc1dc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: flutter_markdown: ">=0.5.2 <0.7.0" timeago: ">=2.0.30 <4.0.0" intl: ">=0.16.1 <0.18.0" - file_picker: ^3.0.1 + file_picker: ^4.1.4 path_provider: ^2.0.1 url_launcher: ^6.0.3 From 0f7b02151122132d47710273a36fcb5579e93b06 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 00:40:53 +0200 Subject: [PATCH 05/25] Implement Desktop file picker --- ...mobileFilePicker.dart => nativeFilePicker.dart} | 0 ...ebDesktopFilePicker.dart => webFilePicker.dart} | 2 +- lib/widgets/sendMessage.dart | 14 +++++++++----- 3 files changed, 10 insertions(+), 6 deletions(-) rename lib/utils/fileInteraction/{mobileFilePicker.dart => nativeFilePicker.dart} (100%) rename lib/utils/fileInteraction/{webDesktopFilePicker.dart => webFilePicker.dart} (97%) diff --git a/lib/utils/fileInteraction/mobileFilePicker.dart b/lib/utils/fileInteraction/nativeFilePicker.dart similarity index 100% rename from lib/utils/fileInteraction/mobileFilePicker.dart rename to lib/utils/fileInteraction/nativeFilePicker.dart diff --git a/lib/utils/fileInteraction/webDesktopFilePicker.dart b/lib/utils/fileInteraction/webFilePicker.dart similarity index 97% rename from lib/utils/fileInteraction/webDesktopFilePicker.dart rename to lib/utils/fileInteraction/webFilePicker.dart index 4d78cb2..4d4faa8 100644 --- a/lib/utils/fileInteraction/webDesktopFilePicker.dart +++ b/lib/utils/fileInteraction/webFilePicker.dart @@ -5,7 +5,7 @@ import 'package:papercups_flutter/models/attachment.dart'; import 'package:papercups_flutter/utils/fileInteraction/uploadFile.dart'; import 'package:papercups_flutter/widgets/alert.dart'; -Future webDesktopFilePicker( +Future webFilePicker( {required BuildContext context, required Function onUploadSuccess, required widget}) async { diff --git a/lib/widgets/sendMessage.dart b/lib/widgets/sendMessage.dart index ccfb68b..8b93e1d 100644 --- a/lib/widgets/sendMessage.dart +++ b/lib/widgets/sendMessage.dart @@ -4,9 +4,9 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:papercups_flutter/utils/fileInteraction/mobileFilePicker.dart'; +import 'package:papercups_flutter/utils/fileInteraction/nativeFilePicker.dart'; import 'package:papercups_flutter/utils/fileInteraction/uploadFile.dart'; -import 'package:papercups_flutter/utils/fileInteraction/webDesktopFilePicker.dart'; +import 'package:papercups_flutter/utils/fileInteraction/webFilePicker.dart'; import '../models/models.dart'; import '../utils/utils.dart'; import '../models/conversation.dart'; @@ -121,7 +121,7 @@ class _SendMessageState extends State { // TODO: Separate this widget // TODO: Internationalize alerts and popups Widget _getFilePicker() { - if (kIsWeb || Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + if (kIsWeb) { return IconButton( splashRadius: 20, icon: Transform.rotate( @@ -131,13 +131,17 @@ class _SendMessageState extends State { size: 18, ), ), - onPressed: () => webDesktopFilePicker( + onPressed: () => webFilePicker( context: context, onUploadSuccess: _onUploadSuccess, widget: widget, ), ); - } else if (Platform.isAndroid || Platform.isIOS) { + } else if (Platform.isAndroid || + Platform.isIOS || + Platform.isWindows || + Platform.isLinux || + Platform.isMacOS) { return PopupMenuButton( icon: Transform.rotate( angle: 0.6, From 44f5335733aa7f356bb68d6c6570ff33482db121 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 00:48:39 +0200 Subject: [PATCH 06/25] Finish Desktop file upload --- lib/utils/fileInteraction/nativeFilePicker.dart | 7 ++++--- lib/utils/fileInteraction/uploadFile.dart | 6 +++++- lib/widgets/sendMessage.dart | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/utils/fileInteraction/nativeFilePicker.dart b/lib/utils/fileInteraction/nativeFilePicker.dart index 5720012..52cf6f6 100644 --- a/lib/utils/fileInteraction/nativeFilePicker.dart +++ b/lib/utils/fileInteraction/nativeFilePicker.dart @@ -5,7 +5,7 @@ import 'package:papercups_flutter/models/models.dart'; import 'package:papercups_flutter/utils/fileInteraction/uploadFile.dart'; import 'package:papercups_flutter/widgets/alert.dart'; -final mobileFilePicker = ( +void nativeFilePicker( {required FileType type, required BuildContext context, required widget, @@ -16,7 +16,6 @@ final mobileFilePicker = ( )) ?.files; if (_paths != null && _paths.first.path != null) { - var context; Alert.show( "Uploading...", context, @@ -51,6 +50,7 @@ final mobileFilePicker = ( gravity: Alert.bottom, duration: Alert.lengthLong, ); + throw _; } catch (_) { Alert.show( "Failed to upload attachment", @@ -60,5 +60,6 @@ final mobileFilePicker = ( gravity: Alert.bottom, duration: Alert.lengthLong, ); + throw _; } -}; +} diff --git a/lib/utils/fileInteraction/uploadFile.dart b/lib/utils/fileInteraction/uploadFile.dart index bbbc558..e063449 100644 --- a/lib/utils/fileInteraction/uploadFile.dart +++ b/lib/utils/fileInteraction/uploadFile.dart @@ -23,7 +23,11 @@ Future> uploadFile( var client = MultipartRequest("POST", uri) ..fields['account_id'] = p.accountId; - if (Platform.isAndroid || Platform.isIOS) { + if (Platform.isAndroid || + Platform.isIOS || + Platform.isLinux || + Platform.isMacOS || + Platform.isWindows) { client.files.add(await MultipartFile.fromPath('file', filePath ?? '')); var msStream = client.finalize(); var totalByteLength = client.contentLength; diff --git a/lib/widgets/sendMessage.dart b/lib/widgets/sendMessage.dart index 8b93e1d..e5f32d6 100644 --- a/lib/widgets/sendMessage.dart +++ b/lib/widgets/sendMessage.dart @@ -150,7 +150,7 @@ class _SendMessageState extends State { size: 18, ), ), - onSelected: (type) => mobileFilePicker( + onSelected: (type) => nativeFilePicker( context: context, onUploadSuccess: _onUploadSuccess, type: type, From e307b46c395aa9e44e61b57fa93772106ba8c4c7 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 00:52:26 +0200 Subject: [PATCH 07/25] lint fixes --- lib/widgets/chat.dart | 2 +- lib/widgets/sendMessage.dart | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/lib/widgets/chat.dart b/lib/widgets/chat.dart index 6721867..fb25f59 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat.dart @@ -50,7 +50,7 @@ class ChatMessages extends StatelessWidget { alignment: Alignment.topCenter, child: NotificationListener( onNotification: (OverscrollIndicatorNotification overscroll) { - overscroll.disallowGlow(); + overscroll.disallowIndicator(); return false; }, child: ListView.builder( diff --git a/lib/widgets/sendMessage.dart b/lib/widgets/sendMessage.dart index e5f32d6..7149d96 100644 --- a/lib/widgets/sendMessage.dart +++ b/lib/widgets/sendMessage.dart @@ -185,19 +185,6 @@ class _SendMessageState extends State { ); } else { return SizedBox(); - // return IconButton( - // icon: Icon(Icons.attach_file), - // onPressed: () { - // Alert.show( - // "file upload is not currently supported for this platform", - // context, - // textStyle: Theme.of(context).textTheme.bodyText2, - // backgroundColor: Colors.red, - // gravity: Alert.bottom, - // duration: Alert.lengthLong, - // ); - // }, - //); } } From 07bd8155438b009426cfc9eb567e92316868faba Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 01:13:55 +0200 Subject: [PATCH 08/25] WIP: Improve file handling --- example/pubspec.lock | 7 + lib/models/attachment.dart | 6 +- lib/models/models.dart | 1 - lib/widgets/chat.dart | 444 +---------------------------------- lib/widgets/chatMessage.dart | 427 +++++++++++++++++++++++++++++++++ lib/widgets/timeWidget.dart | 34 +++ pubspec.lock | 7 + pubspec.yaml | 1 + 8 files changed, 483 insertions(+), 444 deletions(-) create mode 100644 lib/widgets/chatMessage.dart create mode 100644 lib/widgets/timeWidget.dart diff --git a/example/pubspec.lock b/example/pubspec.lock index ce17722..1bbf9d9 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -191,6 +191,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + open_file: + dependency: transitive + description: + name: open_file + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" papercups_flutter: dependency: "direct main" description: diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index a713f66..659c5fa 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -1,8 +1,12 @@ +import 'package:universal_io/io.dart'; + class PapercupsAttachment { String? id; String? fileName; String? fileUrl; String? contentType; + File? file; - PapercupsAttachment({this.id, this.fileName, this.fileUrl, this.contentType}); + PapercupsAttachment( + {this.id, this.fileName, this.fileUrl, this.contentType, this.file}); } diff --git a/lib/models/models.dart b/lib/models/models.dart index 6d7cd0f..49167c6 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -3,6 +3,5 @@ export 'classes.dart'; export 'conversation.dart'; export 'customer.dart'; export 'message.dart'; -export 'message.dart'; export 'user.dart'; export 'attachment.dart'; diff --git a/lib/widgets/chat.dart b/lib/widgets/chat.dart index fb25f59..42a79f0 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat.dart @@ -1,22 +1,8 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; import 'dart:ui'; -import 'package:http/http.dart'; -import 'package:intl/intl.dart'; - import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:papercups_flutter/utils/fileInteraction/downloadFile.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:timeago/timeago.dart' as timeago; -import '../models/models.dart'; -import '../utils/utils.dart'; -import 'widgets.dart'; +import '../models/models.dart'; +import 'chatMessage.dart'; class ChatMessages extends StatelessWidget { final Props props; @@ -80,429 +66,3 @@ class ChatMessages extends StatelessWidget { }); } } - -class ChatMessage extends StatefulWidget { - const ChatMessage({ - Key? key, - required this.msgs, - required this.index, - required this.props, - required this.sending, - required this.maxWidth, - required this.locale, - required this.timeagoLocale, - required this.sendingText, - required this.sentText, - required this.textColor, - this.onMessageBubbleTap, - }) : super(key: key); - - final List? msgs; - final int index; - final Props props; - final bool sending; - final double maxWidth; - final String locale; - final timeagoLocale; - final String sendingText; - final String sentText; - final Color textColor; - final void Function(PapercupsMessage)? onMessageBubbleTap; - - @override - _ChatMessageState createState() => _ChatMessageState(); -} - -class _ChatMessageState extends State { - double opacity = 0; - double maxWidth = 0; - bool isTimeSentVisible = false; - String? longDay; - Timer? timer; - - @override - void dispose() { - if (timer != null) timer!.cancel(); - super.dispose(); - } - - @override - void initState() { - maxWidth = widget.maxWidth; - super.initState(); - } - - void _handleDownloadStream(Stream resp, - {String? filename}) async { - String dir = (await getApplicationDocumentsDirectory()).path; - - List> chunks = []; - int downloaded = 0; - - resp.listen((StreamedResponse r) { - r.stream.listen((List chunk) { - // TODO: Internationlaize this - Alert.show( - "Downloading, ${downloaded / (r.contentLength ?? 1) * 100}% done", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); - - chunks.add(chunk); - downloaded += chunk.length; - }, onDone: () async { - // Alert.show( - // "location: ${dir}/$filename", - // context, - // textStyle: Theme.of(context).textTheme.bodyText2, - // - // gravity: Alert.bottom, - // duration: Alert.lengthLong, - // ); - - File file = File('$dir/$filename'); - - final Uint8List bytes = Uint8List(r.contentLength ?? 0); - int offset = 0; - for (List chunk in chunks) { - bytes.setRange(offset, offset + chunk.length, chunk); - offset += chunk.length; - } - await file.writeAsBytes(bytes); - return; - }); - }); - } - - TimeOfDay senderTime = TimeOfDay.now(); - @override - Widget build(BuildContext context) { - if (opacity == 0) - Timer( - Duration( - milliseconds: 0, - ), () { - if (mounted) - setState(() { - opacity = 1; - }); - }); - var msg = widget.msgs![widget.index]; - - bool userSent = true; - if (msg.userId != null) userSent = false; - - var text = msg.body ?? ""; - if (msg.fileIds != null && msg.fileIds!.isNotEmpty) { - if (text != "") { - text += """ - -"""; - } - text += "> " + msg.attachments!.first.fileName!; - } - var nextMsg = widget.msgs![min(widget.index + 1, widget.msgs!.length - 1)]; - var isLast = widget.index == widget.msgs!.length - 1; - var isFirst = widget.index == 0; - - if (!isLast && - (nextMsg.sentAt!.day != msg.sentAt!.day) && - longDay == null) { - try { - longDay = DateFormat.yMMMMd(widget.locale).format(nextMsg.sentAt!); - } catch (e) { - print("ERROR: Error generating localized date!"); - longDay = "Loading..."; - } - } - if (userSent && isLast && widget.timeagoLocale != null) { - timeago.setLocaleMessages(widget.locale, widget.timeagoLocale); - timeago.setDefaultLocale(widget.locale); - } - if (isLast && userSent && timer == null) - timer = Timer.periodic(Duration(minutes: 1), (timer) { - if (mounted && timer.isActive) { - setState(() {}); - } - }); - if (!isLast && timer != null) timer!.cancel(); - return GestureDetector( - onTap: () async { - setState(() { - isTimeSentVisible = true; - }); - if (widget.onMessageBubbleTap != null) - widget.onMessageBubbleTap!(msg); - else if ((msg.fileIds?.isNotEmpty ?? false)) { - if (kIsWeb) { - String url = msg.attachments?.first.fileUrl ?? ''; - downloadFileWeb(url); - } else if (Platform.isAndroid || Platform.isIOS) { - Stream resp = - await downloadFile(msg.attachments?.first.fileUrl ?? ''); - _handleDownloadStream( - resp, - filename: msg.attachments?.first.fileName, - ); - } - } - }, - onLongPress: () { - HapticFeedback.vibrate(); - print(text); - final data = ClipboardData(text: text); - Clipboard.setData(data); - // TODO: Internationalize this - Alert.show( - "Text copied to clipboard", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); - }, - onTapUp: (_) { - Timer( - Duration( - seconds: 10, - ), () { - if (mounted) - setState(() { - isTimeSentVisible = false; - }); - }); - }, - child: AnimatedOpacity( - curve: Curves.easeIn, - duration: Duration(milliseconds: 300), - opacity: opacity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - userSent ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (!userSent) - Padding( - padding: EdgeInsets.only( - right: 14, - left: 14, - top: (isFirst) ? 15 : 4, - bottom: 5, - ), - child: (widget.msgs!.length == 1 || - nextMsg.userId != msg.userId || - isLast) - ? Container( - decoration: BoxDecoration( - color: widget.props.primaryColor, - gradient: widget.props.primaryGradient, - shape: BoxShape.circle, - ), - child: CircleAvatar( - radius: 16, - backgroundColor: Colors.transparent, - backgroundImage: - (msg.user!.profilePhotoUrl != null) - ? NetworkImage(msg.user!.profilePhotoUrl!) - : null, - child: (msg.user!.profilePhotoUrl != null) - ? null - : (msg.user != null && - msg.user!.fullName == null) - ? Text( - msg.user!.email! - .substring(0, 1) - .toUpperCase(), - style: TextStyle( - color: widget.textColor), - ) - : Text( - msg.user!.fullName! - .substring(0, 1) - .toUpperCase(), - style: TextStyle( - color: widget.textColor), - ), - ), - ) - : SizedBox( - width: 32, - ), - ), - if (userSent) - TimeWidget( - userSent: userSent, - msg: msg, - isVisible: isTimeSentVisible, - ), - Container( - decoration: BoxDecoration( - color: userSent - ? widget.props.primaryColor - : Theme.of(context).brightness == Brightness.light - ? brighten(Theme.of(context).disabledColor, 80) - : Color(0xff282828), - gradient: userSent ? widget.props.primaryGradient : null, - borderRadius: BorderRadius.circular(4), - ), - constraints: BoxConstraints( - maxWidth: maxWidth, - ), - margin: EdgeInsets.only( - top: (isFirst) ? 15 : 4, - bottom: 4, - right: userSent ? 18 : 0, - ), - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 14, - ), - child: MarkdownBody( - data: text, - styleSheet: MarkdownStyleSheet( - blockquote: - TextStyle(decoration: TextDecoration.underline), - p: TextStyle( - color: userSent - ? widget.textColor - : Theme.of(context).textTheme.bodyText1!.color, - ), - a: TextStyle( - color: userSent - ? Colors.white - : Theme.of(context).textTheme.bodyText1!.color, - ), - blockquotePadding: EdgeInsets.only(bottom: 2), - blockquoteDecoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1.5, - color: userSent - ? widget.textColor - : Theme.of(context) - .textTheme - .bodyText1! - .color ?? - Colors.white, - ), - ), - ) - // blockquotePadding: EdgeInsets.only(left: 14), - // blockquoteDecoration: BoxDecoration( - // border: Border( - // left: BorderSide(color: Colors.grey[300]!, width: 4), - // )), - ), - ), - ), - if (!userSent) - TimeWidget( - userSent: userSent, - msg: msg, - isVisible: isTimeSentVisible, - ), - ], - ), - if (!userSent && ((nextMsg.userId != msg.userId) || (isLast))) - Padding( - padding: EdgeInsets.only(left: 16, bottom: 5, top: 4), - child: (msg.user!.fullName == null) - ? Text( - msg.user!.email!, - style: TextStyle( - color: Theme.of(context) - .disabledColor - .withOpacity(0.5), - fontSize: 14, - ), - ) - : Text( - msg.user!.fullName!, - style: TextStyle( - color: Theme.of(context) - .disabledColor - .withOpacity(0.5), - fontSize: 14, - ), - )), - if (userSent && isLast) - Container( - width: double.infinity, - margin: const EdgeInsets.only( - bottom: 4, - left: 18, - right: 18, - ), - child: Text( - widget.sending - ? widget.sendingText - : "${widget.sentText} ${timeago.format(msg.createdAt!)}", - textAlign: TextAlign.end, - style: TextStyle(color: Colors.grey), - ), - ), - if (isLast || nextMsg.userId != msg.userId) - SizedBox( - height: 10, - ), - if (longDay != null) - IgnorePointer( - ignoring: true, - child: Container( - margin: EdgeInsets.all(15), - width: double.infinity, - child: Text( - longDay!, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.grey, - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -class TimeWidget extends StatelessWidget { - const TimeWidget({ - Key? key, - required this.userSent, - required this.msg, - required this.isVisible, - }) : super(key: key); - - final bool userSent; - final PapercupsMessage msg; - final bool isVisible; - - @override - Widget build(BuildContext context) { - return AnimatedOpacity( - opacity: isVisible ? 1 : 0, - duration: Duration(milliseconds: 100), - curve: Curves.easeIn, - child: Padding( - padding: EdgeInsets.only(bottom: 5.0, left: 4, right: 4), - child: Text( - TimeOfDay.fromDateTime(msg.createdAt!).format(context), - style: TextStyle( - color: Theme.of(context).textTheme.bodyText1!.color!.withAlpha(100), - fontSize: 10, - ), - ), - ), - ); - } -} diff --git a/lib/widgets/chatMessage.dart b/lib/widgets/chatMessage.dart new file mode 100644 index 0000000..2f5c164 --- /dev/null +++ b/lib/widgets/chatMessage.dart @@ -0,0 +1,427 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; +import 'package:http/http.dart'; +import 'package:intl/intl.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:open_file/open_file.dart'; +import 'package:papercups_flutter/utils/fileInteraction/downloadFile.dart'; +import 'package:papercups_flutter/widgets/timeWidget.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:timeago/timeago.dart' as timeago; +import '../models/models.dart'; + +import '../utils/utils.dart'; +import 'widgets.dart'; + +class ChatMessage extends StatefulWidget { + const ChatMessage({ + Key? key, + required this.msgs, + required this.index, + required this.props, + required this.sending, + required this.maxWidth, + required this.locale, + required this.timeagoLocale, + required this.sendingText, + required this.sentText, + required this.textColor, + this.onMessageBubbleTap, + }) : super(key: key); + + final List? msgs; + final int index; + final Props props; + final bool sending; + final double maxWidth; + final String locale; + final timeagoLocale; + final String sendingText; + final String sentText; + final Color textColor; + final void Function(PapercupsMessage)? onMessageBubbleTap; + + @override + _ChatMessageState createState() => _ChatMessageState(); +} + +class _ChatMessageState extends State { + double opacity = 0; + double maxWidth = 0; + bool isTimeSentVisible = false; + String? longDay; + Timer? timer; + + @override + void dispose() { + if (timer != null) timer!.cancel(); + super.dispose(); + } + + @override + void initState() { + maxWidth = widget.maxWidth; + super.initState(); + } + + Future _handleDownloadStream(Stream resp, + {String? filename = "noName"}) async { + String dir = (await getApplicationDocumentsDirectory()).path; + File file = File('$dir/$filename'); + + List> chunks = []; + int downloaded = 0; + bool success = false; + + resp.listen((StreamedResponse r) { + r.stream.listen((List chunk) { + // TODO: Internationlaize this + Alert.show( + "Downloading, ${downloaded / (r.contentLength ?? 1) * 100}% done", + context, + textStyle: Theme.of(context).textTheme.bodyText2, + backgroundColor: Theme.of(context).bottomAppBarColor, + gravity: Alert.bottom, + duration: Alert.lengthLong, + ); + + chunks.add(chunk); + downloaded += chunk.length; + }, onDone: () async { + // Alert.show( + // "location: ${dir}/$filename", + // context, + // textStyle: Theme.of(context).textTheme.bodyText2, + // + // gravity: Alert.bottom, + // duration: Alert.lengthLong, + // ); + + final Uint8List bytes = Uint8List(r.contentLength ?? 0); + int offset = 0; + for (List chunk in chunks) { + bytes.setRange(offset, offset + chunk.length, chunk); + offset += chunk.length; + } + await file.writeAsBytes(bytes); + success = true; + return; + }); + }); + + if (success) { + return file; + } + } + + TimeOfDay senderTime = TimeOfDay.now(); + @override + Widget build(BuildContext context) { + if (opacity == 0) + Timer( + Duration( + milliseconds: 0, + ), () { + if (mounted) + setState(() { + opacity = 1; + }); + }); + var msg = widget.msgs![widget.index]; + + bool userSent = true; + if (msg.userId != null) userSent = false; + + var text = msg.body ?? ""; + if (msg.fileIds != null && msg.fileIds!.isNotEmpty) { + if (text != "") { + text += """ + +"""; + } + text += "> " + msg.attachments!.first.fileName!; + } + var nextMsg = widget.msgs![min(widget.index + 1, widget.msgs!.length - 1)]; + var isLast = widget.index == widget.msgs!.length - 1; + var isFirst = widget.index == 0; + + if (!isLast && + (nextMsg.sentAt!.day != msg.sentAt!.day) && + longDay == null) { + try { + longDay = DateFormat.yMMMMd(widget.locale).format(nextMsg.sentAt!); + } catch (e) { + print("ERROR: Error generating localized date!"); + longDay = "Loading..."; + } + } + if (userSent && isLast && widget.timeagoLocale != null) { + timeago.setLocaleMessages(widget.locale, widget.timeagoLocale); + timeago.setDefaultLocale(widget.locale); + } + if (isLast && userSent && timer == null) + timer = Timer.periodic(Duration(minutes: 1), (timer) { + if (mounted && timer.isActive) { + setState(() {}); + } + }); + if (!isLast && timer != null) timer!.cancel(); + return GestureDetector( + onTap: () async { + setState(() { + isTimeSentVisible = true; + }); + if (widget.onMessageBubbleTap != null) + widget.onMessageBubbleTap!(msg); + else if ((msg.fileIds?.isNotEmpty ?? false)) { + if (kIsWeb) { + String url = msg.attachments?.first.fileUrl ?? ''; + downloadFileWeb(url); + } else if (Platform.isAndroid || + Platform.isIOS || + Platform.isLinux || + Platform.isMacOS || + Platform.isWindows) { + Stream resp = + await downloadFile(msg.attachments?.first.fileUrl ?? ''); + final file = await _handleDownloadStream( + resp, + filename: msg.attachments?.first.fileName, + ); + if(file != null && file.existsSync()){ + + } + } + } + }, + onLongPress: () { + HapticFeedback.vibrate(); + print(text); + final data = ClipboardData(text: text); + Clipboard.setData(data); + // TODO: Internationalize this + Alert.show( + "Text copied to clipboard", + context, + textStyle: Theme.of(context).textTheme.bodyText2, + backgroundColor: Theme.of(context).bottomAppBarColor, + gravity: Alert.bottom, + duration: Alert.lengthLong, + ); + }, + onTapUp: (_) { + Timer( + Duration( + seconds: 10, + ), () { + if (mounted) + setState(() { + isTimeSentVisible = false; + }); + }); + }, + child: AnimatedOpacity( + curve: Curves.easeIn, + duration: Duration(milliseconds: 300), + opacity: opacity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + userSent ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!userSent) + Padding( + padding: EdgeInsets.only( + right: 14, + left: 14, + top: (isFirst) ? 15 : 4, + bottom: 5, + ), + child: (widget.msgs!.length == 1 || + nextMsg.userId != msg.userId || + isLast) + ? Container( + decoration: BoxDecoration( + color: widget.props.primaryColor, + gradient: widget.props.primaryGradient, + shape: BoxShape.circle, + ), + child: CircleAvatar( + radius: 16, + backgroundColor: Colors.transparent, + backgroundImage: + (msg.user!.profilePhotoUrl != null) + ? NetworkImage(msg.user!.profilePhotoUrl!) + : null, + child: (msg.user!.profilePhotoUrl != null) + ? null + : (msg.user != null && + msg.user!.fullName == null) + ? Text( + msg.user!.email! + .substring(0, 1) + .toUpperCase(), + style: TextStyle( + color: widget.textColor), + ) + : Text( + msg.user!.fullName! + .substring(0, 1) + .toUpperCase(), + style: TextStyle( + color: widget.textColor), + ), + ), + ) + : SizedBox( + width: 32, + ), + ), + if (userSent) + TimeWidget( + userSent: userSent, + msg: msg, + isVisible: isTimeSentVisible, + ), + Container( + decoration: BoxDecoration( + color: userSent + ? widget.props.primaryColor + : Theme.of(context).brightness == Brightness.light + ? brighten(Theme.of(context).disabledColor, 80) + : Color(0xff282828), + gradient: userSent ? widget.props.primaryGradient : null, + borderRadius: BorderRadius.circular(4), + ), + constraints: BoxConstraints( + maxWidth: maxWidth, + ), + margin: EdgeInsets.only( + top: (isFirst) ? 15 : 4, + bottom: 4, + right: userSent ? 18 : 0, + ), + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 14, + ), + child: MarkdownBody( + data: text, + styleSheet: MarkdownStyleSheet( + blockquote: + TextStyle(decoration: TextDecoration.underline), + p: TextStyle( + color: userSent + ? widget.textColor + : Theme.of(context).textTheme.bodyText1!.color, + ), + a: TextStyle( + color: userSent + ? Colors.white + : Theme.of(context).textTheme.bodyText1!.color, + ), + blockquotePadding: EdgeInsets.only(bottom: 2), + blockquoteDecoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1.5, + color: userSent + ? widget.textColor + : Theme.of(context) + .textTheme + .bodyText1! + .color ?? + Colors.white, + ), + ), + ) + // blockquotePadding: EdgeInsets.only(left: 14), + // blockquoteDecoration: BoxDecoration( + // border: Border( + // left: BorderSide(color: Colors.grey[300]!, width: 4), + // )), + ), + ), + ), + if (!userSent) + TimeWidget( + userSent: userSent, + msg: msg, + isVisible: isTimeSentVisible, + ), + ], + ), + if (!userSent && ((nextMsg.userId != msg.userId) || (isLast))) + Padding( + padding: EdgeInsets.only(left: 16, bottom: 5, top: 4), + child: (msg.user!.fullName == null) + ? Text( + msg.user!.email!, + style: TextStyle( + color: Theme.of(context) + .disabledColor + .withOpacity(0.5), + fontSize: 14, + ), + ) + : Text( + msg.user!.fullName!, + style: TextStyle( + color: Theme.of(context) + .disabledColor + .withOpacity(0.5), + fontSize: 14, + ), + )), + if (userSent && isLast) + Container( + width: double.infinity, + margin: const EdgeInsets.only( + bottom: 4, + left: 18, + right: 18, + ), + child: Text( + widget.sending + ? widget.sendingText + : "${widget.sentText} ${timeago.format(msg.createdAt!)}", + textAlign: TextAlign.end, + style: TextStyle(color: Colors.grey), + ), + ), + if (isLast || nextMsg.userId != msg.userId) + SizedBox( + height: 10, + ), + if (longDay != null) + IgnorePointer( + ignoring: true, + child: Container( + margin: EdgeInsets.all(15), + width: double.infinity, + child: Text( + longDay!, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/timeWidget.dart b/lib/widgets/timeWidget.dart new file mode 100644 index 0000000..4919b88 --- /dev/null +++ b/lib/widgets/timeWidget.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:papercups_flutter/models/models.dart'; + +class TimeWidget extends StatelessWidget { + const TimeWidget({ + Key? key, + required this.userSent, + required this.msg, + required this.isVisible, + }) : super(key: key); + + final bool userSent; + final PapercupsMessage msg; + final bool isVisible; + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + opacity: isVisible ? 1 : 0, + duration: Duration(milliseconds: 100), + curve: Curves.easeIn, + child: Padding( + padding: EdgeInsets.only(bottom: 5.0, left: 4, right: 4), + child: Text( + TimeOfDay.fromDateTime(msg.createdAt!).format(context), + style: TextStyle( + color: Theme.of(context).textTheme.bodyText1!.color!.withAlpha(100), + fontSize: 10, + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index c30ff4a..9024672 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -338,6 +338,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.0.7" + open_file: + dependency: "direct main" + description: + name: open_file + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 87cc1dc..222c438 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: file_picker: ^4.1.4 path_provider: ^2.0.1 url_launcher: ^6.0.3 + open_file: ^3.2.1 dev_dependencies: flutter_test: From 50982c35425866ec8ed7be8c25f772d0da969d04 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 01:15:42 +0200 Subject: [PATCH 09/25] Add copyWith to some classes Necessary for cache implementation --- lib/models/attachment.dart | 16 ++++++++++++++++ lib/models/message.dart | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 659c5fa..e2226e2 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -9,4 +9,20 @@ class PapercupsAttachment { PapercupsAttachment( {this.id, this.fileName, this.fileUrl, this.contentType, this.file}); + + PapercupsAttachment copyWith({ + String? id, + String? fileName, + String? fileUrl, + String? contentType, + File? file, + }) { + return PapercupsAttachment( + id: id ?? this.id, + fileName: fileName ?? this.fileName, + fileUrl: fileUrl ?? this.fileUrl, + contentType: contentType ?? this.contentType, + file: file ?? this.file, + ); + } } diff --git a/lib/models/message.dart b/lib/models/message.dart index 2ce8e01..3a30eaa 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -1,8 +1,8 @@ // Imports import 'package:papercups_flutter/models/models.dart'; -import 'user.dart'; import 'customer.dart'; + export 'user.dart'; /// This class is the class used for each message on the chat. @@ -57,4 +57,36 @@ class PapercupsMessage { this.fileIds, this.attachments, }); + + PapercupsMessage copyWith({ + String? accountId, + String? body, + String? conversationId, + DateTime? createdAt, + String? customerId, + String? id, + DateTime? seenAt, + DateTime? sentAt, + User? user, + PapercupsCustomer? customer, + int? userId, + List? fileIds, + List? attachments, + }) { + return PapercupsMessage( + accountId: accountId ?? this.accountId, + body: body ?? this.body, + conversationId: conversationId ?? this.conversationId, + createdAt: createdAt ?? this.createdAt, + customerId: customerId ?? this.customerId, + id: id ?? this.id, + seenAt: seenAt ?? this.seenAt, + sentAt: sentAt ?? this.sentAt, + user: user ?? this.user, + customer: customer ?? this.customer, + userId: userId ?? this.userId, + fileIds: fileIds ?? this.fileIds, + attachments: attachments ?? this.attachments, + ); + } } From 05e3c274caf2b8371703d26591603d0e2c4a578e Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 01:20:00 +0200 Subject: [PATCH 10/25] Revert "Add copyWith to some classes" This reverts commit 50982c35425866ec8ed7be8c25f772d0da969d04. --- lib/models/attachment.dart | 16 ---------------- lib/models/message.dart | 34 +--------------------------------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index e2226e2..659c5fa 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -9,20 +9,4 @@ class PapercupsAttachment { PapercupsAttachment( {this.id, this.fileName, this.fileUrl, this.contentType, this.file}); - - PapercupsAttachment copyWith({ - String? id, - String? fileName, - String? fileUrl, - String? contentType, - File? file, - }) { - return PapercupsAttachment( - id: id ?? this.id, - fileName: fileName ?? this.fileName, - fileUrl: fileUrl ?? this.fileUrl, - contentType: contentType ?? this.contentType, - file: file ?? this.file, - ); - } } diff --git a/lib/models/message.dart b/lib/models/message.dart index 3a30eaa..2ce8e01 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -1,8 +1,8 @@ // Imports import 'package:papercups_flutter/models/models.dart'; +import 'user.dart'; import 'customer.dart'; - export 'user.dart'; /// This class is the class used for each message on the chat. @@ -57,36 +57,4 @@ class PapercupsMessage { this.fileIds, this.attachments, }); - - PapercupsMessage copyWith({ - String? accountId, - String? body, - String? conversationId, - DateTime? createdAt, - String? customerId, - String? id, - DateTime? seenAt, - DateTime? sentAt, - User? user, - PapercupsCustomer? customer, - int? userId, - List? fileIds, - List? attachments, - }) { - return PapercupsMessage( - accountId: accountId ?? this.accountId, - body: body ?? this.body, - conversationId: conversationId ?? this.conversationId, - createdAt: createdAt ?? this.createdAt, - customerId: customerId ?? this.customerId, - id: id ?? this.id, - seenAt: seenAt ?? this.seenAt, - sentAt: sentAt ?? this.sentAt, - user: user ?? this.user, - customer: customer ?? this.customer, - userId: userId ?? this.userId, - fileIds: fileIds ?? this.fileIds, - attachments: attachments ?? this.attachments, - ); - } } From c0f924cc60df95d1313695e4b4ff398b2ae9ebc5 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 01:20:45 +0200 Subject: [PATCH 11/25] Implement basic file caching for native apps --- lib/models/attachment.dart | 4 +--- lib/widgets/chatMessage.dart | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 659c5fa..51cfaa3 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -5,8 +5,6 @@ class PapercupsAttachment { String? fileName; String? fileUrl; String? contentType; - File? file; - PapercupsAttachment( - {this.id, this.fileName, this.fileUrl, this.contentType, this.file}); + PapercupsAttachment({this.id, this.fileName, this.fileUrl, this.contentType}); } diff --git a/lib/widgets/chatMessage.dart b/lib/widgets/chatMessage.dart index 2f5c164..41b7865 100644 --- a/lib/widgets/chatMessage.dart +++ b/lib/widgets/chatMessage.dart @@ -72,10 +72,7 @@ class _ChatMessageState extends State { } Future _handleDownloadStream(Stream resp, - {String? filename = "noName"}) async { - String dir = (await getApplicationDocumentsDirectory()).path; - File file = File('$dir/$filename'); - + {required File file}) async { List> chunks = []; int downloaded = 0; bool success = false; @@ -189,15 +186,19 @@ class _ChatMessageState extends State { Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + String dir = (await getApplicationDocumentsDirectory()).path; + File? file = + File(dir + (msg.attachments?.first.fileName ?? "noName")); + if (file.existsSync()) { + OpenFile.open(file.absolute.path); + } Stream resp = await downloadFile(msg.attachments?.first.fileUrl ?? ''); - final file = await _handleDownloadStream( + file = await _handleDownloadStream( resp, - filename: msg.attachments?.first.fileName, + file: file, ); - if(file != null && file.existsSync()){ - - } + if (file != null && file.existsSync()) {} } } }, From f0521c6ef45475b7ac66f00d87658622df0f863a Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 02:02:53 +0200 Subject: [PATCH 12/25] WIP: Rework chat attachment --- lib/widgets/chatMessage.dart | 80 ++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/lib/widgets/chatMessage.dart b/lib/widgets/chatMessage.dart index 41b7865..9346317 100644 --- a/lib/widgets/chatMessage.dart +++ b/lib/widgets/chatMessage.dart @@ -59,6 +59,11 @@ class _ChatMessageState extends State { String? longDay; Timer? timer; + // Will only be used if there is a file + bool containsAttachment = false; + int? downloaded; + int? contentLength; + @override void dispose() { if (timer != null) timer!.cancel(); @@ -71,36 +76,21 @@ class _ChatMessageState extends State { super.initState(); } - Future _handleDownloadStream(Stream resp, + Future _handleDownloadStream(Stream resp, {required File file}) async { List> chunks = []; - int downloaded = 0; - bool success = false; resp.listen((StreamedResponse r) { r.stream.listen((List chunk) { - // TODO: Internationlaize this - Alert.show( - "Downloading, ${downloaded / (r.contentLength ?? 1) * 100}% done", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); + if (r.contentLength == null) { + print("Error"); + } + downloaded = 0; + contentLength = r.contentLength; chunks.add(chunk); - downloaded += chunk.length; + downloaded = downloaded! + chunk.length; }, onDone: () async { - // Alert.show( - // "location: ${dir}/$filename", - // context, - // textStyle: Theme.of(context).textTheme.bodyText2, - // - // gravity: Alert.bottom, - // duration: Alert.lengthLong, - // ); - final Uint8List bytes = Uint8List(r.contentLength ?? 0); int offset = 0; for (List chunk in chunks) { @@ -108,13 +98,20 @@ class _ChatMessageState extends State { offset += chunk.length; } await file.writeAsBytes(bytes); - success = true; - return; + OpenFile.open(file.absolute.path); + setState(() {}); }); }); + } - if (success) { - return file; + void checkCachedFiles(PapercupsMessage msg) async { + String dir = (await getApplicationDocumentsDirectory()).path; + File? file = File(dir + + Platform.pathSeparator + + (msg.attachments?.first.fileName ?? TimeOfDay.now().toString())); + if (file.existsSync()) { + downloaded = 1; + contentLength = 1; } } @@ -138,12 +135,8 @@ class _ChatMessageState extends State { var text = msg.body ?? ""; if (msg.fileIds != null && msg.fileIds!.isNotEmpty) { - if (text != "") { - text += """ - -"""; - } - text += "> " + msg.attachments!.first.fileName!; + containsAttachment = true; + checkCachedFiles(msg); } var nextMsg = widget.msgs![min(widget.index + 1, widget.msgs!.length - 1)]; var isLast = widget.index == widget.msgs!.length - 1; @@ -187,18 +180,23 @@ class _ChatMessageState extends State { Platform.isMacOS || Platform.isWindows) { String dir = (await getApplicationDocumentsDirectory()).path; - File? file = - File(dir + (msg.attachments?.first.fileName ?? "noName")); + File? file = File(dir + + Platform.pathSeparator + + (msg.attachments?.first.fileName ?? "noName")); if (file.existsSync()) { + print("Cached at " + file.absolute.path); OpenFile.open(file.absolute.path); + downloaded = 1; + contentLength = 1; + } else { + print("Downloading!"); + Stream resp = + await downloadFile(msg.attachments?.first.fileUrl ?? ''); + _handleDownloadStream( + resp, + file: file, + ); } - Stream resp = - await downloadFile(msg.attachments?.first.fileUrl ?? ''); - file = await _handleDownloadStream( - resp, - file: file, - ); - if (file != null && file.existsSync()) {} } } }, From 983b6b408a98b82a81aa4ca832df8a6160a7eac5 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 11:44:54 +0200 Subject: [PATCH 13/25] WIP refactor attachments --- lib/widgets/chat/attachment.dart | 26 ++ lib/widgets/{ => chat}/chat.dart | 2 +- lib/widgets/chat/chatBubble.dart | 230 ++++++++++ lib/widgets/chat/chatMessage.dart | 247 ++++++++++ lib/widgets/{ => chat}/timeWidget.dart | 0 lib/widgets/chatMessage.dart | 426 ------------------ .../{ => header}/agentAvaiability.dart | 4 +- lib/widgets/{ => header}/header.dart | 2 +- lib/widgets/{ => sendMessage}/poweredBy.dart | 0 .../requireEmailUpfront.dart | 2 +- .../{ => sendMessage}/sendMessage.dart | 16 +- lib/widgets/widgets.dart | 12 +- 12 files changed, 522 insertions(+), 445 deletions(-) create mode 100644 lib/widgets/chat/attachment.dart rename lib/widgets/{ => chat}/chat.dart (98%) create mode 100644 lib/widgets/chat/chatBubble.dart create mode 100644 lib/widgets/chat/chatMessage.dart rename lib/widgets/{ => chat}/timeWidget.dart (100%) delete mode 100644 lib/widgets/chatMessage.dart rename lib/widgets/{ => header}/agentAvaiability.dart (94%) rename lib/widgets/{ => header}/header.dart (98%) rename lib/widgets/{ => sendMessage}/poweredBy.dart (100%) rename lib/widgets/{ => sendMessage}/requireEmailUpfront.dart (99%) rename lib/widgets/{ => sendMessage}/sendMessage.dart (96%) diff --git a/lib/widgets/chat/attachment.dart b/lib/widgets/chat/attachment.dart new file mode 100644 index 0000000..7c6b3ef --- /dev/null +++ b/lib/widgets/chat/attachment.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:papercups_flutter/models/classes.dart'; +import 'package:papercups_flutter/utils/utils.dart'; + +class Attachment extends StatelessWidget { + const Attachment({required this.userSent, required this.props, Key? key}) + : super(key: key); + + final bool userSent; + final Props props; + + @override + Widget build(BuildContext context) { + return Container( + height: 50, + width: double.infinity, + decoration: BoxDecoration( + color: userSent + ? brighten(props.primaryColor!, 20) + : Theme.of(context).brightness == Brightness.light + ? brighten(Theme.of(context).disabledColor, 80) + : Color(0xff282828), + ), + ); + } +} diff --git a/lib/widgets/chat.dart b/lib/widgets/chat/chat.dart similarity index 98% rename from lib/widgets/chat.dart rename to lib/widgets/chat/chat.dart index 42a79f0..ec7bdef 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat/chat.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import '../models/models.dart'; +import '../../models/models.dart'; import 'chatMessage.dart'; class ChatMessages extends StatelessWidget { diff --git a/lib/widgets/chat/chatBubble.dart b/lib/widgets/chat/chatBubble.dart new file mode 100644 index 0000000..b0a21f2 --- /dev/null +++ b/lib/widgets/chat/chatBubble.dart @@ -0,0 +1,230 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:papercups_flutter/models/models.dart'; +import 'package:papercups_flutter/utils/colorMod.dart'; +import 'package:papercups_flutter/widgets/chat/attachment.dart'; +import 'package:papercups_flutter/widgets/chat/timeWidget.dart'; +import 'package:timeago/timeago.dart' as timeago; + +import 'chatMessage.dart'; + +class ChatBubble extends StatelessWidget { + const ChatBubble({ + Key? key, + required this.userSent, + required this.isFirst, + required this.widget, + required this.nextMsg, + required this.msg, + required this.isLast, + required this.isTimeSentVisible, + required this.maxWidth, + required this.text, + required this.longDay, + required this.conatinsAttachment, + }) : super(key: key); + + final bool userSent; + final bool isFirst; + final ChatMessage widget; + final PapercupsMessage nextMsg; + final PapercupsMessage msg; + final bool isLast; + final bool isTimeSentVisible; + final double maxWidth; + final String text; + final String? longDay; + final bool conatinsAttachment; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + userSent ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!userSent) + Padding( + padding: EdgeInsets.only( + right: 14, + left: 14, + top: (isFirst) ? 15 : 4, + bottom: 5, + ), + child: (widget.msgs!.length == 1 || + nextMsg.userId != msg.userId || + isLast) + ? Container( + decoration: BoxDecoration( + color: widget.props.primaryColor, + gradient: widget.props.primaryGradient, + shape: BoxShape.circle, + ), + child: CircleAvatar( + radius: 16, + backgroundColor: Colors.transparent, + backgroundImage: (msg.user!.profilePhotoUrl != null) + ? NetworkImage(msg.user!.profilePhotoUrl!) + : null, + child: (msg.user!.profilePhotoUrl != null) + ? null + : (msg.user != null && msg.user!.fullName == null) + ? Text( + msg.user!.email! + .substring(0, 1) + .toUpperCase(), + style: TextStyle(color: widget.textColor), + ) + : Text( + msg.user!.fullName! + .substring(0, 1) + .toUpperCase(), + style: TextStyle(color: widget.textColor), + ), + ), + ) + : SizedBox( + width: 32, + ), + ), + if (userSent) + TimeWidget( + userSent: userSent, + msg: msg, + isVisible: isTimeSentVisible, + ), + Container( + decoration: BoxDecoration( + color: userSent + ? widget.props.primaryColor + : Theme.of(context).brightness == Brightness.light + ? brighten(Theme.of(context).disabledColor, 80) + : Color(0xff282828), + gradient: userSent ? widget.props.primaryGradient : null, + borderRadius: BorderRadius.circular(4), + ), + constraints: BoxConstraints( + maxWidth: maxWidth, + ), + margin: EdgeInsets.only( + top: (isFirst) ? 15 : 4, + bottom: 4, + right: userSent ? 18 : 0, + ), + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 14, + ), + child: Column( + children: [ + if (conatinsAttachment) + Attachment(userSent: userSent, props: widget.props), + MarkdownBody( + data: text, + styleSheet: MarkdownStyleSheet( + blockquote: + TextStyle(decoration: TextDecoration.underline), + p: TextStyle( + color: userSent + ? widget.textColor + : Theme.of(context).textTheme.bodyText1!.color, + ), + a: TextStyle( + color: userSent + ? Colors.white + : Theme.of(context).textTheme.bodyText1!.color, + ), + blockquotePadding: EdgeInsets.only(bottom: 2), + blockquoteDecoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1.5, + color: userSent + ? widget.textColor + : Theme.of(context) + .textTheme + .bodyText1! + .color ?? + Colors.white, + ), + ), + ) + // blockquotePadding: EdgeInsets.only(left: 14), + // blockquoteDecoration: BoxDecoration( + // border: Border( + // left: BorderSide(color: Colors.grey[300]!, width: 4), + // )), + ), + ), + ], + ), + ), + if (!userSent) + TimeWidget( + userSent: userSent, + msg: msg, + isVisible: isTimeSentVisible, + ), + ], + ), + if (!userSent && ((nextMsg.userId != msg.userId) || (isLast))) + Padding( + padding: EdgeInsets.only(left: 16, bottom: 5, top: 4), + child: (msg.user!.fullName == null) + ? Text( + msg.user!.email!, + style: TextStyle( + color: Theme.of(context).disabledColor.withOpacity(0.5), + fontSize: 14, + ), + ) + : Text( + msg.user!.fullName!, + style: TextStyle( + color: Theme.of(context).disabledColor.withOpacity(0.5), + fontSize: 14, + ), + )), + if (userSent && isLast) + Container( + width: double.infinity, + margin: const EdgeInsets.only( + bottom: 4, + left: 18, + right: 18, + ), + child: Text( + widget.sending + ? widget.sendingText + : "${widget.sentText} ${timeago.format(msg.createdAt!)}", + textAlign: TextAlign.end, + style: TextStyle(color: Colors.grey), + ), + ), + if (isLast || nextMsg.userId != msg.userId) + SizedBox( + height: 10, + ), + if (longDay != null) + IgnorePointer( + ignoring: true, + child: Container( + margin: EdgeInsets.all(15), + width: double.infinity, + child: Text( + longDay!, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/chat/chatMessage.dart b/lib/widgets/chat/chatMessage.dart new file mode 100644 index 0000000..5757074 --- /dev/null +++ b/lib/widgets/chat/chatMessage.dart @@ -0,0 +1,247 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; +import 'package:http/http.dart'; +import 'package:intl/intl.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:open_file/open_file.dart'; +import 'package:papercups_flutter/utils/fileInteraction/downloadFile.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:timeago/timeago.dart' as timeago; +import '../../models/models.dart'; + +import 'chatBubble.dart'; +import '../widgets.dart'; + +class ChatMessage extends StatefulWidget { + const ChatMessage({ + Key? key, + required this.msgs, + required this.index, + required this.props, + required this.sending, + required this.maxWidth, + required this.locale, + required this.timeagoLocale, + required this.sendingText, + required this.sentText, + required this.textColor, + this.onMessageBubbleTap, + }) : super(key: key); + + final List? msgs; + final int index; + final Props props; + final bool sending; + final double maxWidth; + final String locale; + final timeagoLocale; + final String sendingText; + final String sentText; + final Color textColor; + final void Function(PapercupsMessage)? onMessageBubbleTap; + + @override + _ChatMessageState createState() => _ChatMessageState(); +} + +class _ChatMessageState extends State { + double opacity = 0; + double maxWidth = 0; + bool isTimeSentVisible = false; + String? longDay; + Timer? timer; + + // Will only be used if there is a file + bool containsAttachment = false; + int? downloaded; + int? contentLength; + + @override + void dispose() { + if (timer != null) timer!.cancel(); + super.dispose(); + } + + @override + void initState() { + maxWidth = widget.maxWidth; + super.initState(); + } + + Future _handleDownloadStream(Stream resp, + {required File file}) async { + List> chunks = []; + + resp.listen((StreamedResponse r) { + r.stream.listen((List chunk) { + if (r.contentLength == null) { + print("Error"); + } + downloaded = 0; + contentLength = r.contentLength; + + chunks.add(chunk); + downloaded = downloaded! + chunk.length; + }, onDone: () async { + final Uint8List bytes = Uint8List(r.contentLength ?? 0); + int offset = 0; + for (List chunk in chunks) { + bytes.setRange(offset, offset + chunk.length, chunk); + offset += chunk.length; + } + await file.writeAsBytes(bytes); + OpenFile.open(file.absolute.path); + setState(() {}); + }); + }); + } + + void checkCachedFiles(PapercupsMessage msg) async { + String dir = (await getApplicationDocumentsDirectory()).path; + File? file = File(dir + + Platform.pathSeparator + + (msg.attachments?.first.fileName ?? TimeOfDay.now().toString())); + if (file.existsSync()) { + downloaded = 1; + contentLength = 1; + } + } + + TimeOfDay senderTime = TimeOfDay.now(); + @override + Widget build(BuildContext context) { + if (opacity == 0) + Timer( + Duration( + milliseconds: 0, + ), () { + if (mounted) + setState(() { + opacity = 1; + }); + }); + var msg = widget.msgs![widget.index]; + + bool userSent = true; + if (msg.userId != null) userSent = false; + + var text = msg.body ?? ""; + if (msg.fileIds != null && msg.fileIds!.isNotEmpty) { + containsAttachment = true; + checkCachedFiles(msg); + } + var nextMsg = widget.msgs![min(widget.index + 1, widget.msgs!.length - 1)]; + var isLast = widget.index == widget.msgs!.length - 1; + var isFirst = widget.index == 0; + + if (!isLast && + (nextMsg.sentAt!.day != msg.sentAt!.day) && + longDay == null) { + try { + longDay = DateFormat.yMMMMd(widget.locale).format(nextMsg.sentAt!); + } catch (e) { + print("ERROR: Error generating localized date!"); + longDay = "Loading..."; + } + } + if (userSent && isLast && widget.timeagoLocale != null) { + timeago.setLocaleMessages(widget.locale, widget.timeagoLocale); + timeago.setDefaultLocale(widget.locale); + } + if (isLast && userSent && timer == null) + timer = Timer.periodic(Duration(minutes: 1), (timer) { + if (mounted && timer.isActive) { + setState(() {}); + } + }); + if (!isLast && timer != null) timer!.cancel(); + return GestureDetector( + onTap: () async { + setState(() { + isTimeSentVisible = true; + }); + if (widget.onMessageBubbleTap != null) + widget.onMessageBubbleTap!(msg); + else if ((msg.fileIds?.isNotEmpty ?? false)) { + if (kIsWeb) { + String url = msg.attachments?.first.fileUrl ?? ''; + downloadFileWeb(url); + } else if (Platform.isAndroid || + Platform.isIOS || + Platform.isLinux || + Platform.isMacOS || + Platform.isWindows) { + String dir = (await getApplicationDocumentsDirectory()).path; + File? file = File(dir + + Platform.pathSeparator + + (msg.attachments?.first.fileName ?? "noName")); + if (file.existsSync()) { + print("Cached at " + file.absolute.path); + OpenFile.open(file.absolute.path); + downloaded = 1; + contentLength = 1; + } else { + print("Downloading!"); + Stream resp = + await downloadFile(msg.attachments?.first.fileUrl ?? ''); + _handleDownloadStream( + resp, + file: file, + ); + } + } + } + }, + onLongPress: () { + HapticFeedback.vibrate(); + print(text); + final data = ClipboardData(text: text); + Clipboard.setData(data); + // TODO: Internationalize this + Alert.show( + "Text copied to clipboard", + context, + textStyle: Theme.of(context).textTheme.bodyText2, + backgroundColor: Theme.of(context).bottomAppBarColor, + gravity: Alert.bottom, + duration: Alert.lengthLong, + ); + }, + onTapUp: (_) { + Timer( + Duration( + seconds: 10, + ), () { + if (mounted) + setState(() { + isTimeSentVisible = false; + }); + }); + }, + child: AnimatedOpacity( + curve: Curves.easeIn, + duration: Duration(milliseconds: 300), + opacity: opacity, + child: ChatBubble( + userSent: userSent, + isFirst: isFirst, + widget: widget, + nextMsg: nextMsg, + msg: msg, + isLast: isLast, + isTimeSentVisible: isTimeSentVisible, + maxWidth: maxWidth, + text: text, + longDay: longDay, + conatinsAttachment: containsAttachment, + ), + ), + ); + } +} diff --git a/lib/widgets/timeWidget.dart b/lib/widgets/chat/timeWidget.dart similarity index 100% rename from lib/widgets/timeWidget.dart rename to lib/widgets/chat/timeWidget.dart diff --git a/lib/widgets/chatMessage.dart b/lib/widgets/chatMessage.dart deleted file mode 100644 index 9346317..0000000 --- a/lib/widgets/chatMessage.dart +++ /dev/null @@ -1,426 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; -import 'dart:ui'; -import 'package:http/http.dart'; -import 'package:intl/intl.dart'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:open_file/open_file.dart'; -import 'package:papercups_flutter/utils/fileInteraction/downloadFile.dart'; -import 'package:papercups_flutter/widgets/timeWidget.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:timeago/timeago.dart' as timeago; -import '../models/models.dart'; - -import '../utils/utils.dart'; -import 'widgets.dart'; - -class ChatMessage extends StatefulWidget { - const ChatMessage({ - Key? key, - required this.msgs, - required this.index, - required this.props, - required this.sending, - required this.maxWidth, - required this.locale, - required this.timeagoLocale, - required this.sendingText, - required this.sentText, - required this.textColor, - this.onMessageBubbleTap, - }) : super(key: key); - - final List? msgs; - final int index; - final Props props; - final bool sending; - final double maxWidth; - final String locale; - final timeagoLocale; - final String sendingText; - final String sentText; - final Color textColor; - final void Function(PapercupsMessage)? onMessageBubbleTap; - - @override - _ChatMessageState createState() => _ChatMessageState(); -} - -class _ChatMessageState extends State { - double opacity = 0; - double maxWidth = 0; - bool isTimeSentVisible = false; - String? longDay; - Timer? timer; - - // Will only be used if there is a file - bool containsAttachment = false; - int? downloaded; - int? contentLength; - - @override - void dispose() { - if (timer != null) timer!.cancel(); - super.dispose(); - } - - @override - void initState() { - maxWidth = widget.maxWidth; - super.initState(); - } - - Future _handleDownloadStream(Stream resp, - {required File file}) async { - List> chunks = []; - - resp.listen((StreamedResponse r) { - r.stream.listen((List chunk) { - if (r.contentLength == null) { - print("Error"); - } - downloaded = 0; - contentLength = r.contentLength; - - chunks.add(chunk); - downloaded = downloaded! + chunk.length; - }, onDone: () async { - final Uint8List bytes = Uint8List(r.contentLength ?? 0); - int offset = 0; - for (List chunk in chunks) { - bytes.setRange(offset, offset + chunk.length, chunk); - offset += chunk.length; - } - await file.writeAsBytes(bytes); - OpenFile.open(file.absolute.path); - setState(() {}); - }); - }); - } - - void checkCachedFiles(PapercupsMessage msg) async { - String dir = (await getApplicationDocumentsDirectory()).path; - File? file = File(dir + - Platform.pathSeparator + - (msg.attachments?.first.fileName ?? TimeOfDay.now().toString())); - if (file.existsSync()) { - downloaded = 1; - contentLength = 1; - } - } - - TimeOfDay senderTime = TimeOfDay.now(); - @override - Widget build(BuildContext context) { - if (opacity == 0) - Timer( - Duration( - milliseconds: 0, - ), () { - if (mounted) - setState(() { - opacity = 1; - }); - }); - var msg = widget.msgs![widget.index]; - - bool userSent = true; - if (msg.userId != null) userSent = false; - - var text = msg.body ?? ""; - if (msg.fileIds != null && msg.fileIds!.isNotEmpty) { - containsAttachment = true; - checkCachedFiles(msg); - } - var nextMsg = widget.msgs![min(widget.index + 1, widget.msgs!.length - 1)]; - var isLast = widget.index == widget.msgs!.length - 1; - var isFirst = widget.index == 0; - - if (!isLast && - (nextMsg.sentAt!.day != msg.sentAt!.day) && - longDay == null) { - try { - longDay = DateFormat.yMMMMd(widget.locale).format(nextMsg.sentAt!); - } catch (e) { - print("ERROR: Error generating localized date!"); - longDay = "Loading..."; - } - } - if (userSent && isLast && widget.timeagoLocale != null) { - timeago.setLocaleMessages(widget.locale, widget.timeagoLocale); - timeago.setDefaultLocale(widget.locale); - } - if (isLast && userSent && timer == null) - timer = Timer.periodic(Duration(minutes: 1), (timer) { - if (mounted && timer.isActive) { - setState(() {}); - } - }); - if (!isLast && timer != null) timer!.cancel(); - return GestureDetector( - onTap: () async { - setState(() { - isTimeSentVisible = true; - }); - if (widget.onMessageBubbleTap != null) - widget.onMessageBubbleTap!(msg); - else if ((msg.fileIds?.isNotEmpty ?? false)) { - if (kIsWeb) { - String url = msg.attachments?.first.fileUrl ?? ''; - downloadFileWeb(url); - } else if (Platform.isAndroid || - Platform.isIOS || - Platform.isLinux || - Platform.isMacOS || - Platform.isWindows) { - String dir = (await getApplicationDocumentsDirectory()).path; - File? file = File(dir + - Platform.pathSeparator + - (msg.attachments?.first.fileName ?? "noName")); - if (file.existsSync()) { - print("Cached at " + file.absolute.path); - OpenFile.open(file.absolute.path); - downloaded = 1; - contentLength = 1; - } else { - print("Downloading!"); - Stream resp = - await downloadFile(msg.attachments?.first.fileUrl ?? ''); - _handleDownloadStream( - resp, - file: file, - ); - } - } - } - }, - onLongPress: () { - HapticFeedback.vibrate(); - print(text); - final data = ClipboardData(text: text); - Clipboard.setData(data); - // TODO: Internationalize this - Alert.show( - "Text copied to clipboard", - context, - textStyle: Theme.of(context).textTheme.bodyText2, - backgroundColor: Theme.of(context).bottomAppBarColor, - gravity: Alert.bottom, - duration: Alert.lengthLong, - ); - }, - onTapUp: (_) { - Timer( - Duration( - seconds: 10, - ), () { - if (mounted) - setState(() { - isTimeSentVisible = false; - }); - }); - }, - child: AnimatedOpacity( - curve: Curves.easeIn, - duration: Duration(milliseconds: 300), - opacity: opacity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - userSent ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (!userSent) - Padding( - padding: EdgeInsets.only( - right: 14, - left: 14, - top: (isFirst) ? 15 : 4, - bottom: 5, - ), - child: (widget.msgs!.length == 1 || - nextMsg.userId != msg.userId || - isLast) - ? Container( - decoration: BoxDecoration( - color: widget.props.primaryColor, - gradient: widget.props.primaryGradient, - shape: BoxShape.circle, - ), - child: CircleAvatar( - radius: 16, - backgroundColor: Colors.transparent, - backgroundImage: - (msg.user!.profilePhotoUrl != null) - ? NetworkImage(msg.user!.profilePhotoUrl!) - : null, - child: (msg.user!.profilePhotoUrl != null) - ? null - : (msg.user != null && - msg.user!.fullName == null) - ? Text( - msg.user!.email! - .substring(0, 1) - .toUpperCase(), - style: TextStyle( - color: widget.textColor), - ) - : Text( - msg.user!.fullName! - .substring(0, 1) - .toUpperCase(), - style: TextStyle( - color: widget.textColor), - ), - ), - ) - : SizedBox( - width: 32, - ), - ), - if (userSent) - TimeWidget( - userSent: userSent, - msg: msg, - isVisible: isTimeSentVisible, - ), - Container( - decoration: BoxDecoration( - color: userSent - ? widget.props.primaryColor - : Theme.of(context).brightness == Brightness.light - ? brighten(Theme.of(context).disabledColor, 80) - : Color(0xff282828), - gradient: userSent ? widget.props.primaryGradient : null, - borderRadius: BorderRadius.circular(4), - ), - constraints: BoxConstraints( - maxWidth: maxWidth, - ), - margin: EdgeInsets.only( - top: (isFirst) ? 15 : 4, - bottom: 4, - right: userSent ? 18 : 0, - ), - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 14, - ), - child: MarkdownBody( - data: text, - styleSheet: MarkdownStyleSheet( - blockquote: - TextStyle(decoration: TextDecoration.underline), - p: TextStyle( - color: userSent - ? widget.textColor - : Theme.of(context).textTheme.bodyText1!.color, - ), - a: TextStyle( - color: userSent - ? Colors.white - : Theme.of(context).textTheme.bodyText1!.color, - ), - blockquotePadding: EdgeInsets.only(bottom: 2), - blockquoteDecoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1.5, - color: userSent - ? widget.textColor - : Theme.of(context) - .textTheme - .bodyText1! - .color ?? - Colors.white, - ), - ), - ) - // blockquotePadding: EdgeInsets.only(left: 14), - // blockquoteDecoration: BoxDecoration( - // border: Border( - // left: BorderSide(color: Colors.grey[300]!, width: 4), - // )), - ), - ), - ), - if (!userSent) - TimeWidget( - userSent: userSent, - msg: msg, - isVisible: isTimeSentVisible, - ), - ], - ), - if (!userSent && ((nextMsg.userId != msg.userId) || (isLast))) - Padding( - padding: EdgeInsets.only(left: 16, bottom: 5, top: 4), - child: (msg.user!.fullName == null) - ? Text( - msg.user!.email!, - style: TextStyle( - color: Theme.of(context) - .disabledColor - .withOpacity(0.5), - fontSize: 14, - ), - ) - : Text( - msg.user!.fullName!, - style: TextStyle( - color: Theme.of(context) - .disabledColor - .withOpacity(0.5), - fontSize: 14, - ), - )), - if (userSent && isLast) - Container( - width: double.infinity, - margin: const EdgeInsets.only( - bottom: 4, - left: 18, - right: 18, - ), - child: Text( - widget.sending - ? widget.sendingText - : "${widget.sentText} ${timeago.format(msg.createdAt!)}", - textAlign: TextAlign.end, - style: TextStyle(color: Colors.grey), - ), - ), - if (isLast || nextMsg.userId != msg.userId) - SizedBox( - height: 10, - ), - if (longDay != null) - IgnorePointer( - ignoring: true, - child: Container( - margin: EdgeInsets.all(15), - width: double.infinity, - child: Text( - longDay!, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.grey, - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/widgets/agentAvaiability.dart b/lib/widgets/header/agentAvaiability.dart similarity index 94% rename from lib/widgets/agentAvaiability.dart rename to lib/widgets/header/agentAvaiability.dart index d97594f..37c40f5 100644 --- a/lib/widgets/agentAvaiability.dart +++ b/lib/widgets/header/agentAvaiability.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../models/models.dart'; -import '../utils/utils.dart'; +import '../../models/models.dart'; +import '../../utils/utils.dart'; class AgentAvailability extends StatelessWidget { final Props props; diff --git a/lib/widgets/header.dart b/lib/widgets/header/header.dart similarity index 98% rename from lib/widgets/header.dart rename to lib/widgets/header/header.dart index 9dcf817..7f52d1d 100644 --- a/lib/widgets/header.dart +++ b/lib/widgets/header/header.dart @@ -1,7 +1,7 @@ // Imports import 'package:flutter/material.dart'; -import '../models/classes.dart'; +import '../../models/classes.dart'; /// This header is shown at the top of the widget and can be customised. class Header extends StatelessWidget { diff --git a/lib/widgets/poweredBy.dart b/lib/widgets/sendMessage/poweredBy.dart similarity index 100% rename from lib/widgets/poweredBy.dart rename to lib/widgets/sendMessage/poweredBy.dart diff --git a/lib/widgets/requireEmailUpfront.dart b/lib/widgets/sendMessage/requireEmailUpfront.dart similarity index 99% rename from lib/widgets/requireEmailUpfront.dart rename to lib/widgets/sendMessage/requireEmailUpfront.dart index 3e6ba7b..db0e6b6 100644 --- a/lib/widgets/requireEmailUpfront.dart +++ b/lib/widgets/sendMessage/requireEmailUpfront.dart @@ -1,6 +1,6 @@ // Imports import 'package:flutter/material.dart'; -import '../models/models.dart'; +import '../../models/models.dart'; /// Requires email upfront. class RequireEmailUpfront extends StatefulWidget { diff --git a/lib/widgets/sendMessage.dart b/lib/widgets/sendMessage/sendMessage.dart similarity index 96% rename from lib/widgets/sendMessage.dart rename to lib/widgets/sendMessage/sendMessage.dart index 7149d96..e04e71d 100644 --- a/lib/widgets/sendMessage.dart +++ b/lib/widgets/sendMessage/sendMessage.dart @@ -7,16 +7,16 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:papercups_flutter/utils/fileInteraction/nativeFilePicker.dart'; import 'package:papercups_flutter/utils/fileInteraction/uploadFile.dart'; import 'package:papercups_flutter/utils/fileInteraction/webFilePicker.dart'; -import '../models/models.dart'; -import '../utils/utils.dart'; -import '../models/conversation.dart'; -import '../models/customer.dart'; -import '../utils/apiInteraction/getConversationDetails.dart'; -import '../utils/apiInteraction/getCustomerDetails.dart'; +import '../../models/models.dart'; +import '../../utils/utils.dart'; +import '../../models/conversation.dart'; +import '../../models/customer.dart'; +import '../../utils/apiInteraction/getConversationDetails.dart'; +import '../../utils/apiInteraction/getCustomerDetails.dart'; import 'package:phoenix_socket/phoenix_socket.dart'; -import '../models/classes.dart'; -import 'alert.dart'; +import '../../models/classes.dart'; +import '../alert.dart'; /// Send message text box. class SendMessage extends StatefulWidget { diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 1207ffc..b1fabf0 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,8 +1,8 @@ /// Exports for the widgets, new widgets should be added here and this is the file that should be imported instead of the specific widget. -export 'agentAvaiability.dart'; -export 'chat.dart'; -export 'header.dart'; -export 'sendMessage.dart'; -export 'poweredBy.dart'; -export 'requireEmailUpfront.dart'; +export 'header/agentAvaiability.dart'; +export 'chat/chat.dart'; +export 'header/header.dart'; +export 'sendMessage/sendMessage.dart'; +export 'sendMessage/poweredBy.dart'; +export 'sendMessage/requireEmailUpfront.dart'; export 'alert.dart'; From 95252be186b383b0cbfad8c059e6dd3bf57793e9 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 16:53:01 +0200 Subject: [PATCH 14/25] Support attachments on recieved messages --- lib/utils/socket/joinConversation.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/utils/socket/joinConversation.dart b/lib/utils/socket/joinConversation.dart index c404599..6cac98a 100644 --- a/lib/utils/socket/joinConversation.dart +++ b/lib/utils/socket/joinConversation.dart @@ -42,6 +42,23 @@ PhoenixChannel? joinConversationAndListen({ conversationId: event.payload!["conversation_id"], customerId: event.payload!["customer_id"], id: event.payload!["id"], + attachments: (event.payload!["attachments"] != null) + ? (event.payload!["attachments"] as List) + .map((attachment) { + return PapercupsAttachment( + contentType: attachment["content_type"], + fileName: attachment["filename"], + fileUrl: attachment["file_url"], + id: attachment["id"], + ); + }).toList() + : null, + fileIds: (event.payload!["attachments"] != null) + ? (event.payload!["attachments"] as List) + .map((attachment) { + return attachment["id"] as String; + }).toList() + : null, user: (event.payload!["user"] != null) ? User( email: event.payload!["user"]["email"], From 486663f8bc050559ca27b6f7f9944c0af2d89261 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 17:50:09 +0200 Subject: [PATCH 15/25] Work on attachments --- lib/widgets/chat/attachment.dart | 42 ++++++++++++++-- lib/widgets/chat/chatBubble.dart | 83 +++++++++++++++++-------------- lib/widgets/chat/chatMessage.dart | 25 ++++++---- 3 files changed, 99 insertions(+), 51 deletions(-) diff --git a/lib/widgets/chat/attachment.dart b/lib/widgets/chat/attachment.dart index 7c6b3ef..6f7694b 100644 --- a/lib/widgets/chat/attachment.dart +++ b/lib/widgets/chat/attachment.dart @@ -3,24 +3,58 @@ import 'package:papercups_flutter/models/classes.dart'; import 'package:papercups_flutter/utils/utils.dart'; class Attachment extends StatelessWidget { - const Attachment({required this.userSent, required this.props, Key? key}) + const Attachment( + {required this.userSent, + required this.props, + required this.fileName, + required this.textColor, + required this.msgHasText, + required this.isDownloaded, + Key? key}) : super(key: key); final bool userSent; final Props props; + final String fileName; + final Color textColor; + final bool msgHasText; + final bool isDownloaded; @override Widget build(BuildContext context) { return Container( - height: 50, width: double.infinity, decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), color: userSent - ? brighten(props.primaryColor!, 20) + ? darken(props.primaryColor!, 20) : Theme.of(context).brightness == Brightness.light - ? brighten(Theme.of(context).disabledColor, 80) + ? brighten(Theme.of(context).disabledColor, 70) : Color(0xff282828), ), + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10), + margin: EdgeInsets.only(bottom: msgHasText ? 0 : 10), + child: Row( + children: [ + CircleAvatar( + backgroundColor: props.primaryColor, + child: Icon( + !isDownloaded + ? Icons.download_for_offline_rounded + : Icons.attach_file_rounded, + color: Theme.of(context).canvasColor, + ), + ), + SizedBox(width: 10), + Text( + fileName, + style: TextStyle( + color: userSent + ? textColor + : Theme.of(context).textTheme.bodyText1!.color), + ) + ], + ), ); } } diff --git a/lib/widgets/chat/chatBubble.dart b/lib/widgets/chat/chatBubble.dart index b0a21f2..28895e4 100644 --- a/lib/widgets/chat/chatBubble.dart +++ b/lib/widgets/chat/chatBubble.dart @@ -22,6 +22,7 @@ class ChatBubble extends StatelessWidget { required this.text, required this.longDay, required this.conatinsAttachment, + required this.isDownloaded, }) : super(key: key); final bool userSent; @@ -35,6 +36,7 @@ class ChatBubble extends StatelessWidget { final String text; final String? longDay; final bool conatinsAttachment; + final bool isDownloaded; @override Widget build(BuildContext context) { @@ -120,46 +122,55 @@ class ChatBubble extends StatelessWidget { horizontal: 14, ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (conatinsAttachment) - Attachment(userSent: userSent, props: widget.props), - MarkdownBody( - data: text, - styleSheet: MarkdownStyleSheet( - blockquote: - TextStyle(decoration: TextDecoration.underline), - p: TextStyle( - color: userSent - ? widget.textColor - : Theme.of(context).textTheme.bodyText1!.color, - ), - a: TextStyle( - color: userSent - ? Colors.white - : Theme.of(context).textTheme.bodyText1!.color, - ), - blockquotePadding: EdgeInsets.only(bottom: 2), - blockquoteDecoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1.5, - color: userSent - ? widget.textColor - : Theme.of(context) - .textTheme - .bodyText1! - .color ?? - Colors.white, + Attachment( + userSent: userSent, + props: widget.props, + fileName: msg.attachments!.first.fileName ?? "No Name", + textColor: widget.textColor, + msgHasText: msg.body != null, + isDownloaded: isDownloaded, + ), + if (msg.body != "null") + MarkdownBody( + data: text, + styleSheet: MarkdownStyleSheet( + blockquote: + TextStyle(decoration: TextDecoration.underline), + p: TextStyle( + color: userSent + ? widget.textColor + : Theme.of(context).textTheme.bodyText1!.color, + ), + a: TextStyle( + color: userSent + ? Colors.white + : Theme.of(context).textTheme.bodyText1!.color, + ), + blockquotePadding: EdgeInsets.only(bottom: 2), + blockquoteDecoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1.5, + color: userSent + ? widget.textColor + : Theme.of(context) + .textTheme + .bodyText1! + .color ?? + Colors.white, + ), ), + ) + // blockquotePadding: EdgeInsets.only(left: 14), + // blockquoteDecoration: BoxDecoration( + // border: Border( + // left: BorderSide(color: Colors.grey[300]!, width: 4), + // )), ), - ) - // blockquotePadding: EdgeInsets.only(left: 14), - // blockquoteDecoration: BoxDecoration( - // border: Border( - // left: BorderSide(color: Colors.grey[300]!, width: 4), - // )), - ), - ), + ), ], ), ), diff --git a/lib/widgets/chat/chatMessage.dart b/lib/widgets/chat/chatMessage.dart index 5757074..5a31f20 100644 --- a/lib/widgets/chat/chatMessage.dart +++ b/lib/widgets/chat/chatMessage.dart @@ -59,8 +59,8 @@ class _ChatMessageState extends State { // Will only be used if there is a file bool containsAttachment = false; - int? downloaded; - int? contentLength; + int? downloaded = 0; + int? contentLength = 1; @override void dispose() { @@ -102,12 +102,18 @@ class _ChatMessageState extends State { }); } - void checkCachedFiles(PapercupsMessage msg) async { + Future getAttachment(PapercupsAttachment attachment) async { String dir = (await getApplicationDocumentsDirectory()).path; File? file = File(dir + Platform.pathSeparator + - (msg.attachments?.first.fileName ?? TimeOfDay.now().toString())); - if (file.existsSync()) { + (attachment.id ?? "noId") + + (attachment.fileName ?? "noName")); + return file; + } + + Future checkCachedFiles(PapercupsAttachment attachment) async { + var file = await getAttachment(attachment); + if (await file.exists()) { downloaded = 1; contentLength = 1; } @@ -134,7 +140,7 @@ class _ChatMessageState extends State { var text = msg.body ?? ""; if (msg.fileIds != null && msg.fileIds!.isNotEmpty) { containsAttachment = true; - checkCachedFiles(msg); + checkCachedFiles(msg.attachments!.first); } var nextMsg = widget.msgs![min(widget.index + 1, widget.msgs!.length - 1)]; var isLast = widget.index == widget.msgs!.length - 1; @@ -177,10 +183,7 @@ class _ChatMessageState extends State { Platform.isLinux || Platform.isMacOS || Platform.isWindows) { - String dir = (await getApplicationDocumentsDirectory()).path; - File? file = File(dir + - Platform.pathSeparator + - (msg.attachments?.first.fileName ?? "noName")); + var file = await getAttachment(msg.attachments!.first); if (file.existsSync()) { print("Cached at " + file.absolute.path); OpenFile.open(file.absolute.path); @@ -200,7 +203,6 @@ class _ChatMessageState extends State { }, onLongPress: () { HapticFeedback.vibrate(); - print(text); final data = ClipboardData(text: text); Clipboard.setData(data); // TODO: Internationalize this @@ -240,6 +242,7 @@ class _ChatMessageState extends State { text: text, longDay: longDay, conatinsAttachment: containsAttachment, + isDownloaded: downloaded == contentLength, ), ), ); From fe4e2467b0e5748b12a551aa026161e3044777fe Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 18:01:35 +0200 Subject: [PATCH 16/25] Finish attachment handling --- lib/widgets/chat/attachment.dart | 33 ++++++++++++++++++++++--------- lib/widgets/chat/chatBubble.dart | 3 +++ lib/widgets/chat/chatMessage.dart | 8 +++++++- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/lib/widgets/chat/attachment.dart b/lib/widgets/chat/attachment.dart index 6f7694b..20840ab 100644 --- a/lib/widgets/chat/attachment.dart +++ b/lib/widgets/chat/attachment.dart @@ -10,6 +10,7 @@ class Attachment extends StatelessWidget { required this.textColor, required this.msgHasText, required this.isDownloaded, + required this.downloading, Key? key}) : super(key: key); @@ -19,6 +20,7 @@ class Attachment extends StatelessWidget { final Color textColor; final bool msgHasText; final bool isDownloaded; + final bool downloading; @override Widget build(BuildContext context) { @@ -38,20 +40,33 @@ class Attachment extends StatelessWidget { children: [ CircleAvatar( backgroundColor: props.primaryColor, - child: Icon( - !isDownloaded - ? Icons.download_for_offline_rounded - : Icons.attach_file_rounded, - color: Theme.of(context).canvasColor, + child: Stack( + alignment: Alignment.center, + children: [ + if (downloading) + CircularProgressIndicator( + color: Theme.of(context).canvasColor, + ), + Icon( + !isDownloaded + ? Icons.download_for_offline_rounded + : Icons.attach_file_rounded, + color: Theme.of(context).canvasColor, + ), + ], ), ), - SizedBox(width: 10), + SizedBox( + width: 10, + ), Text( fileName, style: TextStyle( - color: userSent - ? textColor - : Theme.of(context).textTheme.bodyText1!.color), + color: userSent + ? textColor + : Theme.of(context).textTheme.bodyText1!.color, + ), + overflow: TextOverflow.ellipsis, ) ], ), diff --git a/lib/widgets/chat/chatBubble.dart b/lib/widgets/chat/chatBubble.dart index 28895e4..1d8b6b5 100644 --- a/lib/widgets/chat/chatBubble.dart +++ b/lib/widgets/chat/chatBubble.dart @@ -23,6 +23,7 @@ class ChatBubble extends StatelessWidget { required this.longDay, required this.conatinsAttachment, required this.isDownloaded, + required this.downloading, }) : super(key: key); final bool userSent; @@ -37,6 +38,7 @@ class ChatBubble extends StatelessWidget { final String? longDay; final bool conatinsAttachment; final bool isDownloaded; + final bool downloading; @override Widget build(BuildContext context) { @@ -132,6 +134,7 @@ class ChatBubble extends StatelessWidget { textColor: widget.textColor, msgHasText: msg.body != null, isDownloaded: isDownloaded, + downloading: downloading, ), if (msg.body != "null") MarkdownBody( diff --git a/lib/widgets/chat/chatMessage.dart b/lib/widgets/chat/chatMessage.dart index 5a31f20..074917a 100644 --- a/lib/widgets/chat/chatMessage.dart +++ b/lib/widgets/chat/chatMessage.dart @@ -61,6 +61,7 @@ class _ChatMessageState extends State { bool containsAttachment = false; int? downloaded = 0; int? contentLength = 1; + bool downloading = false; @override void dispose() { @@ -78,6 +79,9 @@ class _ChatMessageState extends State { {required File file}) async { List> chunks = []; + downloading = true; + setState(() {}); + resp.listen((StreamedResponse r) { r.stream.listen((List chunk) { if (r.contentLength == null) { @@ -97,6 +101,7 @@ class _ChatMessageState extends State { } await file.writeAsBytes(bytes); OpenFile.open(file.absolute.path); + downloading = false; setState(() {}); }); }); @@ -174,7 +179,7 @@ class _ChatMessageState extends State { }); if (widget.onMessageBubbleTap != null) widget.onMessageBubbleTap!(msg); - else if ((msg.fileIds?.isNotEmpty ?? false)) { + else if ((msg.fileIds?.isNotEmpty ?? false) && !downloading) { if (kIsWeb) { String url = msg.attachments?.first.fileUrl ?? ''; downloadFileWeb(url); @@ -243,6 +248,7 @@ class _ChatMessageState extends State { longDay: longDay, conatinsAttachment: containsAttachment, isDownloaded: downloaded == contentLength, + downloading: downloading, ), ), ); From 6a7962ff66f2cdb6af1fb86bc14b067929d762ca Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 18:12:58 +0200 Subject: [PATCH 17/25] Finish attachment handling in UI --- lib/widgets/chat/attachment.dart | 16 +++++++++------- lib/widgets/chat/chatMessage.dart | 2 ++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/widgets/chat/attachment.dart b/lib/widgets/chat/attachment.dart index 20840ab..f10ca6c 100644 --- a/lib/widgets/chat/attachment.dart +++ b/lib/widgets/chat/attachment.dart @@ -59,14 +59,16 @@ class Attachment extends StatelessWidget { SizedBox( width: 10, ), - Text( - fileName, - style: TextStyle( - color: userSent - ? textColor - : Theme.of(context).textTheme.bodyText1!.color, + Expanded( + child: Text( + fileName, + style: TextStyle( + color: userSent + ? textColor + : Theme.of(context).textTheme.bodyText1!.color, + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, ) ], ), diff --git a/lib/widgets/chat/chatMessage.dart b/lib/widgets/chat/chatMessage.dart index 074917a..cc1f08f 100644 --- a/lib/widgets/chat/chatMessage.dart +++ b/lib/widgets/chat/chatMessage.dart @@ -102,6 +102,8 @@ class _ChatMessageState extends State { await file.writeAsBytes(bytes); OpenFile.open(file.absolute.path); downloading = false; + contentLength = 1; + downloaded = 1; setState(() {}); }); }); From 2c8c76149330f3ddafa00e2d98ea1cfc55fe41cc Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 18:35:11 +0200 Subject: [PATCH 18/25] Fix breaking API change --- lib/utils/apiInteraction/getPastCustomerMessages.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/apiInteraction/getPastCustomerMessages.dart b/lib/utils/apiInteraction/getPastCustomerMessages.dart index 90ce021..d9727da 100644 --- a/lib/utils/apiInteraction/getPastCustomerMessages.dart +++ b/lib/utils/apiInteraction/getPastCustomerMessages.dart @@ -38,7 +38,7 @@ Future> getPastCustomerMessages( }; } - data[0]["messages"].forEach((val) { + data["messages"].forEach((val) { rMsgs.add( PapercupsMessage( accountId: val["account_id"], From 8a88413dda2e09e59b841c8167643683aed43c55 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 18:45:31 +0200 Subject: [PATCH 19/25] switch to bool system --- lib/widgets/chat/chatMessage.dart | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/widgets/chat/chatMessage.dart b/lib/widgets/chat/chatMessage.dart index cc1f08f..699d2cf 100644 --- a/lib/widgets/chat/chatMessage.dart +++ b/lib/widgets/chat/chatMessage.dart @@ -59,8 +59,7 @@ class _ChatMessageState extends State { // Will only be used if there is a file bool containsAttachment = false; - int? downloaded = 0; - int? contentLength = 1; + bool downloaded = false; bool downloading = false; @override @@ -87,11 +86,8 @@ class _ChatMessageState extends State { if (r.contentLength == null) { print("Error"); } - downloaded = 0; - contentLength = r.contentLength; chunks.add(chunk); - downloaded = downloaded! + chunk.length; }, onDone: () async { final Uint8List bytes = Uint8List(r.contentLength ?? 0); int offset = 0; @@ -102,8 +98,7 @@ class _ChatMessageState extends State { await file.writeAsBytes(bytes); OpenFile.open(file.absolute.path); downloading = false; - contentLength = 1; - downloaded = 1; + downloaded = true; setState(() {}); }); }); @@ -121,8 +116,7 @@ class _ChatMessageState extends State { Future checkCachedFiles(PapercupsAttachment attachment) async { var file = await getAttachment(attachment); if (await file.exists()) { - downloaded = 1; - contentLength = 1; + downloaded = true; } } @@ -194,8 +188,7 @@ class _ChatMessageState extends State { if (file.existsSync()) { print("Cached at " + file.absolute.path); OpenFile.open(file.absolute.path); - downloaded = 1; - contentLength = 1; + downloaded = true; } else { print("Downloading!"); Stream resp = @@ -249,7 +242,7 @@ class _ChatMessageState extends State { text: text, longDay: longDay, conatinsAttachment: containsAttachment, - isDownloaded: downloaded == contentLength, + isDownloaded: downloaded, downloading: downloading, ), ), From d797679364bcd1cc7dc1f7b51759817b4acb6570 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 19:10:24 +0200 Subject: [PATCH 20/25] handle mutiple attachments --- .../fileInteraction/handleDownloads.dart | 57 +++++++ lib/widgets/chat/attachment.dart | 147 ++++++++++++------ lib/widgets/chat/chatBubble.dart | 24 ++- lib/widgets/chat/chatMessage.dart | 81 +--------- 4 files changed, 169 insertions(+), 140 deletions(-) create mode 100644 lib/utils/fileInteraction/handleDownloads.dart diff --git a/lib/utils/fileInteraction/handleDownloads.dart b/lib/utils/fileInteraction/handleDownloads.dart new file mode 100644 index 0000000..6d1de48 --- /dev/null +++ b/lib/utils/fileInteraction/handleDownloads.dart @@ -0,0 +1,57 @@ +import 'dart:typed_data'; + +import 'package:http/http.dart'; +import 'package:open_file/open_file.dart'; +import 'package:papercups_flutter/models/models.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:universal_io/io.dart'; + +Future handleDownloadStream(Stream resp, + {required File file, + Function? onDownloading, + Function? onDownloaded}) async { + List> chunks = []; + + if (onDownloading != null) { + onDownloading(); + } + + resp.listen((StreamedResponse r) { + r.stream.listen((List chunk) { + if (r.contentLength == null) { + print("Error"); + } + + chunks.add(chunk); + }, onDone: () async { + final Uint8List bytes = Uint8List(r.contentLength ?? 0); + int offset = 0; + for (List chunk in chunks) { + bytes.setRange(offset, offset + chunk.length, chunk); + offset += chunk.length; + } + await file.writeAsBytes(bytes); + OpenFile.open(file.absolute.path); + if (onDownloaded != null) { + onDownloaded(); + } + }); + }); +} + +Future getAttachment(PapercupsAttachment attachment) async { + String dir = (await getApplicationDocumentsDirectory()).path; + File? file = File(dir + + Platform.pathSeparator + + (attachment.id ?? "noId") + + (attachment.fileName ?? "noName")); + return file; +} + +Future checkCachedFiles(PapercupsAttachment attachment) async { + var file = await getAttachment(attachment); + if (await file.exists()) { + return true; + } + return false; +} diff --git a/lib/widgets/chat/attachment.dart b/lib/widgets/chat/attachment.dart index f10ca6c..791a8a8 100644 --- a/lib/widgets/chat/attachment.dart +++ b/lib/widgets/chat/attachment.dart @@ -1,16 +1,22 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart'; +import 'package:open_file/open_file.dart'; +import 'package:papercups_flutter/models/attachment.dart'; import 'package:papercups_flutter/models/classes.dart'; +import 'package:papercups_flutter/utils/fileInteraction/downloadFile.dart'; +import 'package:papercups_flutter/utils/fileInteraction/handleDownloads.dart'; import 'package:papercups_flutter/utils/utils.dart'; +import 'package:universal_io/io.dart'; -class Attachment extends StatelessWidget { +class Attachment extends StatefulWidget { const Attachment( {required this.userSent, required this.props, required this.fileName, required this.textColor, required this.msgHasText, - required this.isDownloaded, - required this.downloading, + required this.attachment, Key? key}) : super(key: key); @@ -19,58 +25,103 @@ class Attachment extends StatelessWidget { final String fileName; final Color textColor; final bool msgHasText; - final bool isDownloaded; - final bool downloading; + final PapercupsAttachment attachment; + + @override + State createState() => _AttachmentState(); +} + +class _AttachmentState extends State { + bool downloading = false; + bool downloaded = false; @override Widget build(BuildContext context) { - return Container( - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: userSent - ? darken(props.primaryColor!, 20) - : Theme.of(context).brightness == Brightness.light - ? brighten(Theme.of(context).disabledColor, 70) - : Color(0xff282828), - ), - padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10), - margin: EdgeInsets.only(bottom: msgHasText ? 0 : 10), - child: Row( - children: [ - CircleAvatar( - backgroundColor: props.primaryColor, - child: Stack( - alignment: Alignment.center, - children: [ - if (downloading) - CircularProgressIndicator( + checkCachedFiles(widget.attachment).then((value) { + if (value) { + downloaded = true; + setState(() {}); + } + }); + + return InkWell( + onTap: () async { + if (kIsWeb) { + String url = widget.attachment.fileUrl ?? ''; + downloadFileWeb(url); + } else if (Platform.isAndroid || + Platform.isIOS || + Platform.isLinux || + Platform.isMacOS || + Platform.isWindows) { + var file = await getAttachment(widget.attachment); + if (file.existsSync()) { + print("Cached at " + file.absolute.path); + OpenFile.open(file.absolute.path); + downloaded = true; + } else { + Stream resp = + await downloadFile(widget.attachment.fileUrl ?? ''); + handleDownloadStream(resp, file: file, onDownloaded: () { + downloaded = true; + downloading = false; + setState(() {}); + }, onDownloading: () { + downloaded = false; + downloading = true; + setState(() {}); + }); + } + } + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: widget.userSent + ? darken(widget.props.primaryColor!, 20) + : Theme.of(context).brightness == Brightness.light + ? brighten(Theme.of(context).disabledColor, 70) + : Color(0xff282828), + ), + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10), + margin: EdgeInsets.symmetric(vertical: !widget.msgHasText ? 0 : 5), + child: Row( + children: [ + CircleAvatar( + backgroundColor: widget.props.primaryColor, + child: Stack( + alignment: Alignment.center, + children: [ + if (downloading) + CircularProgressIndicator( + color: Theme.of(context).canvasColor, + ), + Icon( + !downloaded + ? Icons.download_for_offline_rounded + : Icons.attach_file_rounded, color: Theme.of(context).canvasColor, ), - Icon( - !isDownloaded - ? Icons.download_for_offline_rounded - : Icons.attach_file_rounded, - color: Theme.of(context).canvasColor, - ), - ], - ), - ), - SizedBox( - width: 10, - ), - Expanded( - child: Text( - fileName, - style: TextStyle( - color: userSent - ? textColor - : Theme.of(context).textTheme.bodyText1!.color, + ], ), - overflow: TextOverflow.ellipsis, ), - ) - ], + SizedBox( + width: 10, + ), + Expanded( + child: Text( + widget.fileName, + style: TextStyle( + color: widget.userSent + ? widget.textColor + : Theme.of(context).textTheme.bodyText1!.color, + ), + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), ), ); } diff --git a/lib/widgets/chat/chatBubble.dart b/lib/widgets/chat/chatBubble.dart index 1d8b6b5..238c380 100644 --- a/lib/widgets/chat/chatBubble.dart +++ b/lib/widgets/chat/chatBubble.dart @@ -22,8 +22,6 @@ class ChatBubble extends StatelessWidget { required this.text, required this.longDay, required this.conatinsAttachment, - required this.isDownloaded, - required this.downloading, }) : super(key: key); final bool userSent; @@ -37,8 +35,6 @@ class ChatBubble extends StatelessWidget { final String text; final String? longDay; final bool conatinsAttachment; - final bool isDownloaded; - final bool downloading; @override Widget build(BuildContext context) { @@ -127,15 +123,17 @@ class ChatBubble extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (conatinsAttachment) - Attachment( - userSent: userSent, - props: widget.props, - fileName: msg.attachments!.first.fileName ?? "No Name", - textColor: widget.textColor, - msgHasText: msg.body != null, - isDownloaded: isDownloaded, - downloading: downloading, - ), + ...msg.attachments!.map((e) { + return Attachment( + userSent: userSent, + props: widget.props, + fileName: e.fileName ?? "No Name", + textColor: widget.textColor, + msgHasText: + (msg.attachments!.length > 1 || msg.body != null), + attachment: e, + ); + }).toList(), if (msg.body != "null") MarkdownBody( data: text, diff --git a/lib/widgets/chat/chatMessage.dart b/lib/widgets/chat/chatMessage.dart index 699d2cf..9ed11b1 100644 --- a/lib/widgets/chat/chatMessage.dart +++ b/lib/widgets/chat/chatMessage.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:open_file/open_file.dart'; import 'package:papercups_flutter/utils/fileInteraction/downloadFile.dart'; +import 'package:papercups_flutter/utils/fileInteraction/handleDownloads.dart'; import 'package:path_provider/path_provider.dart'; import 'package:timeago/timeago.dart' as timeago; import '../../models/models.dart'; @@ -57,10 +58,7 @@ class _ChatMessageState extends State { String? longDay; Timer? timer; - // Will only be used if there is a file bool containsAttachment = false; - bool downloaded = false; - bool downloading = false; @override void dispose() { @@ -74,52 +72,6 @@ class _ChatMessageState extends State { super.initState(); } - Future _handleDownloadStream(Stream resp, - {required File file}) async { - List> chunks = []; - - downloading = true; - setState(() {}); - - resp.listen((StreamedResponse r) { - r.stream.listen((List chunk) { - if (r.contentLength == null) { - print("Error"); - } - - chunks.add(chunk); - }, onDone: () async { - final Uint8List bytes = Uint8List(r.contentLength ?? 0); - int offset = 0; - for (List chunk in chunks) { - bytes.setRange(offset, offset + chunk.length, chunk); - offset += chunk.length; - } - await file.writeAsBytes(bytes); - OpenFile.open(file.absolute.path); - downloading = false; - downloaded = true; - setState(() {}); - }); - }); - } - - Future getAttachment(PapercupsAttachment attachment) async { - String dir = (await getApplicationDocumentsDirectory()).path; - File? file = File(dir + - Platform.pathSeparator + - (attachment.id ?? "noId") + - (attachment.fileName ?? "noName")); - return file; - } - - Future checkCachedFiles(PapercupsAttachment attachment) async { - var file = await getAttachment(attachment); - if (await file.exists()) { - downloaded = true; - } - } - TimeOfDay senderTime = TimeOfDay.now(); @override Widget build(BuildContext context) { @@ -141,7 +93,6 @@ class _ChatMessageState extends State { var text = msg.body ?? ""; if (msg.fileIds != null && msg.fileIds!.isNotEmpty) { containsAttachment = true; - checkCachedFiles(msg.attachments!.first); } var nextMsg = widget.msgs![min(widget.index + 1, widget.msgs!.length - 1)]; var isLast = widget.index == widget.msgs!.length - 1; @@ -173,33 +124,7 @@ class _ChatMessageState extends State { setState(() { isTimeSentVisible = true; }); - if (widget.onMessageBubbleTap != null) - widget.onMessageBubbleTap!(msg); - else if ((msg.fileIds?.isNotEmpty ?? false) && !downloading) { - if (kIsWeb) { - String url = msg.attachments?.first.fileUrl ?? ''; - downloadFileWeb(url); - } else if (Platform.isAndroid || - Platform.isIOS || - Platform.isLinux || - Platform.isMacOS || - Platform.isWindows) { - var file = await getAttachment(msg.attachments!.first); - if (file.existsSync()) { - print("Cached at " + file.absolute.path); - OpenFile.open(file.absolute.path); - downloaded = true; - } else { - print("Downloading!"); - Stream resp = - await downloadFile(msg.attachments?.first.fileUrl ?? ''); - _handleDownloadStream( - resp, - file: file, - ); - } - } - } + if (widget.onMessageBubbleTap != null) widget.onMessageBubbleTap!(msg); }, onLongPress: () { HapticFeedback.vibrate(); @@ -242,8 +167,6 @@ class _ChatMessageState extends State { text: text, longDay: longDay, conatinsAttachment: containsAttachment, - isDownloaded: downloaded, - downloading: downloading, ), ), ); From a5e935a29c77afc3d60717082af5a4579383959f Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 20:45:35 +0200 Subject: [PATCH 21/25] Support past attachments --- .../apiInteraction/getPastCustomerMessages.dart | 15 +++++++++++++++ lib/widgets/chat/attachment.dart | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/utils/apiInteraction/getPastCustomerMessages.dart b/lib/utils/apiInteraction/getPastCustomerMessages.dart index d9727da..524e725 100644 --- a/lib/utils/apiInteraction/getPastCustomerMessages.dart +++ b/lib/utils/apiInteraction/getPastCustomerMessages.dart @@ -63,6 +63,21 @@ Future> getPastCustomerMessages( : null, ) : null, + attachments: (val["attachments"] != null) + ? (val["attachments"] as List).map((attachment) { + return PapercupsAttachment( + contentType: attachment["content_type"], + fileName: attachment["filename"], + fileUrl: attachment["file_url"], + id: attachment["id"], + ); + }).toList() + : null, + fileIds: (val["attachments"] != null) + ? (val["attachments"] as List).map((attachment) { + return attachment["id"] as String; + }).toList() + : null, ), ); }); diff --git a/lib/widgets/chat/attachment.dart b/lib/widgets/chat/attachment.dart index 791a8a8..2807ab2 100644 --- a/lib/widgets/chat/attachment.dart +++ b/lib/widgets/chat/attachment.dart @@ -40,7 +40,9 @@ class _AttachmentState extends State { checkCachedFiles(widget.attachment).then((value) { if (value) { downloaded = true; - setState(() {}); + if (mounted) { + setState(() {}); + } } }); From 9297b983337a25a37739311694d60ce83766bc2b Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 20:52:56 +0200 Subject: [PATCH 22/25] support uploading --- lib/widgets/chat/attachment.dart | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/widgets/chat/attachment.dart b/lib/widgets/chat/attachment.dart index 2807ab2..46c79b5 100644 --- a/lib/widgets/chat/attachment.dart +++ b/lib/widgets/chat/attachment.dart @@ -34,6 +34,7 @@ class Attachment extends StatefulWidget { class _AttachmentState extends State { bool downloading = false; bool downloaded = false; + bool uploading = false; @override Widget build(BuildContext context) { @@ -48,14 +49,16 @@ class _AttachmentState extends State { return InkWell( onTap: () async { - if (kIsWeb) { + if (kIsWeb && !uploading) { String url = widget.attachment.fileUrl ?? ''; downloadFileWeb(url); - } else if (Platform.isAndroid || - Platform.isIOS || - Platform.isLinux || - Platform.isMacOS || - Platform.isWindows) { + } else if ((Platform.isAndroid || + Platform.isIOS || + Platform.isLinux || + Platform.isMacOS || + Platform.isWindows) && + !downloading && + !uploading) { var file = await getAttachment(widget.attachment); if (file.existsSync()) { print("Cached at " + file.absolute.path); @@ -95,13 +98,15 @@ class _AttachmentState extends State { child: Stack( alignment: Alignment.center, children: [ - if (downloading) + if (downloading || uploading) CircularProgressIndicator( color: Theme.of(context).canvasColor, ), Icon( !downloaded - ? Icons.download_for_offline_rounded + ? uploading + ? Icons.upload_rounded + : Icons.download_rounded : Icons.attach_file_rounded, color: Theme.of(context).canvasColor, ), From 609b40068fe6ad144d462d419ceb54ca8a2096dd Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 20:54:00 +0200 Subject: [PATCH 23/25] Round type selection --- lib/widgets/sendMessage/sendMessage.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/widgets/sendMessage/sendMessage.dart b/lib/widgets/sendMessage/sendMessage.dart index e04e71d..0329cf9 100644 --- a/lib/widgets/sendMessage/sendMessage.dart +++ b/lib/widgets/sendMessage/sendMessage.dart @@ -143,6 +143,7 @@ class _SendMessageState extends State { Platform.isLinux || Platform.isMacOS) { return PopupMenuButton( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), icon: Transform.rotate( angle: 0.6, child: Icon( From ed84315e0c1dae410e2fbc5126e546547870db81 Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 21:02:14 +0200 Subject: [PATCH 24/25] WIP refactor uploading to be handled in-message --- .../fileInteraction/nativeFilePicker.dart | 8 ++++++- lib/widgets/sendMessage/sendMessage.dart | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/utils/fileInteraction/nativeFilePicker.dart b/lib/utils/fileInteraction/nativeFilePicker.dart index 52cf6f6..11be48f 100644 --- a/lib/utils/fileInteraction/nativeFilePicker.dart +++ b/lib/utils/fileInteraction/nativeFilePicker.dart @@ -9,13 +9,19 @@ void nativeFilePicker( {required FileType type, required BuildContext context, required widget, - required Function onUploadSuccess}) async { + required Function onUploadSuccess, + required Function onUploadStarted}) async { try { final _paths = (await FilePicker.platform.pickFiles( type: type, )) ?.files; if (_paths != null && _paths.first.path != null) { + onUploadStarted(_paths + .map((e) => PapercupsAttachment( + fileName: e.name, + )) + .toList()); Alert.show( "Uploading...", context, diff --git a/lib/widgets/sendMessage/sendMessage.dart b/lib/widgets/sendMessage/sendMessage.dart index 0329cf9..ee8fbfc 100644 --- a/lib/widgets/sendMessage/sendMessage.dart +++ b/lib/widgets/sendMessage/sendMessage.dart @@ -154,6 +154,27 @@ class _SendMessageState extends State { onSelected: (type) => nativeFilePicker( context: context, onUploadSuccess: _onUploadSuccess, + onUploadStarted: (List files) { + List fileIds = files.map((e) => e.id ?? "").toList(); + _sendMessage( + _msgFocusNode, + _msgController, + widget.customer, + widget.props, + widget.setCustomer, + widget.conversation, + widget.setConversation, + widget.setConversationChannel, + widget.conversationChannel, + widget.socket, + widget.setState, + widget.messages, + widget.sending, + files, + fileIds, + true, + ); + }, type: type, widget: widget, ), From be1699c37a3b4a96d553784f631968728d29c2fc Mon Sep 17 00:00:00 2001 From: Aguilaair Date: Sun, 17 Oct 2021 23:05:23 +0200 Subject: [PATCH 25/25] Revert "WIP refactor uploading to be handled in-message" This reverts commit ed84315e0c1dae410e2fbc5126e546547870db81. --- .../fileInteraction/nativeFilePicker.dart | 8 +------ lib/widgets/sendMessage/sendMessage.dart | 21 ------------------- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/lib/utils/fileInteraction/nativeFilePicker.dart b/lib/utils/fileInteraction/nativeFilePicker.dart index 11be48f..52cf6f6 100644 --- a/lib/utils/fileInteraction/nativeFilePicker.dart +++ b/lib/utils/fileInteraction/nativeFilePicker.dart @@ -9,19 +9,13 @@ void nativeFilePicker( {required FileType type, required BuildContext context, required widget, - required Function onUploadSuccess, - required Function onUploadStarted}) async { + required Function onUploadSuccess}) async { try { final _paths = (await FilePicker.platform.pickFiles( type: type, )) ?.files; if (_paths != null && _paths.first.path != null) { - onUploadStarted(_paths - .map((e) => PapercupsAttachment( - fileName: e.name, - )) - .toList()); Alert.show( "Uploading...", context, diff --git a/lib/widgets/sendMessage/sendMessage.dart b/lib/widgets/sendMessage/sendMessage.dart index ee8fbfc..0329cf9 100644 --- a/lib/widgets/sendMessage/sendMessage.dart +++ b/lib/widgets/sendMessage/sendMessage.dart @@ -154,27 +154,6 @@ class _SendMessageState extends State { onSelected: (type) => nativeFilePicker( context: context, onUploadSuccess: _onUploadSuccess, - onUploadStarted: (List files) { - List fileIds = files.map((e) => e.id ?? "").toList(); - _sendMessage( - _msgFocusNode, - _msgController, - widget.customer, - widget.props, - widget.setCustomer, - widget.conversation, - widget.setConversation, - widget.setConversationChannel, - widget.conversationChannel, - widget.socket, - widget.setState, - widget.messages, - widget.sending, - files, - fileIds, - true, - ); - }, type: type, widget: widget, ),