diff --git a/example/pubspec.lock b/example/pubspec.lock index cff939f..1bbf9d9 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 @@ -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: @@ -438,4 +445,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/lib/models/attachment.dart b/lib/models/attachment.dart index a713f66..51cfaa3 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -1,3 +1,5 @@ +import 'package:universal_io/io.dart'; + class PapercupsAttachment { String? id; String? fileName; 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/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 69% rename from lib/utils/getPastCustomerMessages.dart rename to lib/utils/apiInteraction/getPastCustomerMessages.dart index 9b9796b..524e725 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( @@ -38,7 +38,7 @@ Future> getPastCustomerMessages( }; } - data[0]["messages"].forEach((val) { + data["messages"].forEach((val) { rMsgs.add( PapercupsMessage( accountId: val["account_id"], @@ -55,10 +55,29 @@ 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, + 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/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/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/utils/fileInteraction/nativeFilePicker.dart b/lib/utils/fileInteraction/nativeFilePicker.dart new file mode 100644 index 0000000..52cf6f6 --- /dev/null +++ b/lib/utils/fileInteraction/nativeFilePicker.dart @@ -0,0 +1,65 @@ +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/fileInteraction/uploadFile.dart'; +import 'package:papercups_flutter/widgets/alert.dart'; + +void nativeFilePicker( + {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) { + 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, + ); + throw _; + } 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, + ); + throw _; + } +} diff --git a/lib/utils/uploadFile.dart b/lib/utils/fileInteraction/uploadFile.dart similarity index 94% rename from lib/utils/uploadFile.dart rename to lib/utils/fileInteraction/uploadFile.dart index 1c0850e..e063449 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); @@ -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/utils/fileInteraction/webFilePicker.dart b/lib/utils/fileInteraction/webFilePicker.dart new file mode 100644 index 0000000..4d4faa8 --- /dev/null +++ b/lib/utils/fileInteraction/webFilePicker.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 webFilePicker( + {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/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 77% rename from lib/utils/joinConversation.dart rename to lib/utils/socket/joinConversation.dart index dac49b2..6cac98a 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({ @@ -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"], 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 deleted file mode 100644 index 90aeaad..0000000 --- a/lib/widgets/chat.dart +++ /dev/null @@ -1,508 +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:papercups_flutter/utils/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'; - -class ChatMessages extends StatelessWidget { - final Props props; - final List? messages; - final bool sending; - final ScrollController _controller; - final String locale; - final timeagoLocale; - final String sendingText; - final String sentText; - final Color textColor; - final void Function(PapercupsMessage)? onMessageBubbleTap; - - ChatMessages( - this.props, - this.messages, - this._controller, - this.sending, - this.locale, - this.timeagoLocale, - this.sendingText, - this.sentText, - this.textColor, - this.onMessageBubbleTap, { - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, layout) { - return Container( - alignment: Alignment.topCenter, - child: NotificationListener( - onNotification: (OverscrollIndicatorNotification overscroll) { - overscroll.disallowGlow(); - return false; - }, - child: ListView.builder( - controller: _controller, - physics: props.scrollEnabled - ? ClampingScrollPhysics() - : NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - itemCount: messages!.length, - itemBuilder: (context, index) { - return ChatMessage( - msgs: messages, - index: index, - props: props, - sending: sending, - locale: locale, - timeagoLocale: timeagoLocale, - maxWidth: layout.maxWidth * 0.65, - sendingText: sendingText, - sentText: sentText, - textColor: textColor, - ); - }, - ), - ), - ); - }); - } -} - -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/chat/attachment.dart b/lib/widgets/chat/attachment.dart new file mode 100644 index 0000000..46c79b5 --- /dev/null +++ b/lib/widgets/chat/attachment.dart @@ -0,0 +1,135 @@ +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 StatefulWidget { + const Attachment( + {required this.userSent, + required this.props, + required this.fileName, + required this.textColor, + required this.msgHasText, + required this.attachment, + Key? key}) + : super(key: key); + + final bool userSent; + final Props props; + final String fileName; + final Color textColor; + final bool msgHasText; + final PapercupsAttachment attachment; + + @override + State createState() => _AttachmentState(); +} + +class _AttachmentState extends State { + bool downloading = false; + bool downloaded = false; + bool uploading = false; + + @override + Widget build(BuildContext context) { + checkCachedFiles(widget.attachment).then((value) { + if (value) { + downloaded = true; + if (mounted) { + setState(() {}); + } + } + }); + + return InkWell( + onTap: () async { + if (kIsWeb && !uploading) { + String url = widget.attachment.fileUrl ?? ''; + downloadFileWeb(url); + } 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); + 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 || uploading) + CircularProgressIndicator( + color: Theme.of(context).canvasColor, + ), + Icon( + !downloaded + ? uploading + ? Icons.upload_rounded + : Icons.download_rounded + : Icons.attach_file_rounded, + color: Theme.of(context).canvasColor, + ), + ], + ), + ), + 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/chat.dart b/lib/widgets/chat/chat.dart new file mode 100644 index 0000000..ec7bdef --- /dev/null +++ b/lib/widgets/chat/chat.dart @@ -0,0 +1,68 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; + +import '../../models/models.dart'; +import 'chatMessage.dart'; + +class ChatMessages extends StatelessWidget { + final Props props; + final List? messages; + final bool sending; + final ScrollController _controller; + final String locale; + final timeagoLocale; + final String sendingText; + final String sentText; + final Color textColor; + final void Function(PapercupsMessage)? onMessageBubbleTap; + + ChatMessages( + this.props, + this.messages, + this._controller, + this.sending, + this.locale, + this.timeagoLocale, + this.sendingText, + this.sentText, + this.textColor, + this.onMessageBubbleTap, { + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, layout) { + return Container( + alignment: Alignment.topCenter, + child: NotificationListener( + onNotification: (OverscrollIndicatorNotification overscroll) { + overscroll.disallowIndicator(); + return false; + }, + child: ListView.builder( + controller: _controller, + physics: props.scrollEnabled + ? ClampingScrollPhysics() + : NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: messages!.length, + itemBuilder: (context, index) { + return ChatMessage( + msgs: messages, + index: index, + props: props, + sending: sending, + locale: locale, + timeagoLocale: timeagoLocale, + maxWidth: layout.maxWidth * 0.65, + sendingText: sendingText, + sentText: sentText, + textColor: textColor, + ); + }, + ), + ), + ); + }); + } +} diff --git a/lib/widgets/chat/chatBubble.dart b/lib/widgets/chat/chatBubble.dart new file mode 100644 index 0000000..238c380 --- /dev/null +++ b/lib/widgets/chat/chatBubble.dart @@ -0,0 +1,242 @@ +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( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (conatinsAttachment) + ...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, + 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..9ed11b1 --- /dev/null +++ b/lib/widgets/chat/chatMessage.dart @@ -0,0 +1,174 @@ +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:papercups_flutter/utils/fileInteraction/handleDownloads.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; + + bool containsAttachment = false; + + @override + void dispose() { + if (timer != null) timer!.cancel(); + super.dispose(); + } + + @override + void initState() { + maxWidth = widget.maxWidth; + super.initState(); + } + + 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; + } + 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); + }, + onLongPress: () { + HapticFeedback.vibrate(); + 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/chat/timeWidget.dart b/lib/widgets/chat/timeWidget.dart new file mode 100644 index 0000000..4919b88 --- /dev/null +++ b/lib/widgets/chat/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/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 67% rename from lib/widgets/sendMessage.dart rename to lib/widgets/sendMessage/sendMessage.dart index 00cc0de..0329cf9 100644 --- a/lib/widgets/sendMessage.dart +++ b/lib/widgets/sendMessage/sendMessage.dart @@ -3,19 +3,20 @@ 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/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 '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 '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 { @@ -130,40 +131,19 @@ 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: () => 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( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), icon: Transform.rotate( angle: 0.6, child: Icon( @@ -171,58 +151,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) => nativeFilePicker( + context: context, + onUploadSuccess: _onUploadSuccess, + type: type, + widget: widget, + ), itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: FileType.any, @@ -252,19 +186,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, - // ); - // }, - //); } } 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'; diff --git a/pubspec.lock b/pubspec.lock index add7064..9024672 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 @@ -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: @@ -646,4 +653,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..222c438 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,9 +16,10 @@ 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 + open_file: ^3.2.1 dev_dependencies: flutter_test: 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';