Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #58 #66

Merged
merged 14 commits into from
May 23, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:3.6.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand Down
2 changes: 1 addition & 1 deletion example/android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
1 change: 1 addition & 0 deletions example/android/settings_aar.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ':app'
8 changes: 8 additions & 0 deletions lib/models/attachment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class PapercupsAttachment {
String? id;
String? fileName;
String? fileUrl;
String? contentType;

PapercupsAttachment({this.id, this.fileName, this.fileUrl, this.contentType});
}
10 changes: 10 additions & 0 deletions lib/models/message.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Imports
import 'package:papercups_flutter/models/models.dart';

import 'user.dart';
import 'customer.dart';
export 'user.dart';
Expand Down Expand Up @@ -34,6 +36,12 @@ class PapercupsMessage {
/// The userID of the person sending. Is nullable is the person sending is a customer.
int? userId;

/// The file ids of files to be sent, could be null if message does not contain files
List<String>? fileIds;

/// the metadata of files attached
List<PapercupsAttachment>? attachments;

PapercupsMessage({
this.accountId,
this.body,
Expand All @@ -46,5 +54,7 @@ class PapercupsMessage {
this.user,
this.userId,
this.customer,
this.fileIds,
this.attachments,
});
}
1 change: 1 addition & 0 deletions lib/models/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export 'customer.dart';
export 'message.dart';
export 'message.dart';
export 'user.dart';
export 'attachment.dart';
5 changes: 5 additions & 0 deletions lib/papercups_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class PaperCupsWidget extends StatefulWidget {
/// Set to true in order to make the send message section float
final bool floatingSendMessage;

/// Function to handle message bubble tap action
final void Function(PapercupsMessage)? onMessageBubbleTap;

PaperCupsWidget({
required this.props,
this.dateLocale = "en-US",
Expand All @@ -46,6 +49,7 @@ class PaperCupsWidget extends StatefulWidget {
this.sentText = "Sent",
this.closeAction,
this.floatingSendMessage = false,
this.onMessageBubbleTap,
});

@override
Expand Down Expand Up @@ -299,6 +303,7 @@ class _PaperCupsWidgetState extends State<PaperCupsWidget> {
widget.sendingText,
widget.sentText,
textColor,
widget.onMessageBubbleTap,
),
),
if (!widget.floatingSendMessage) PoweredBy(),
Expand Down
13 changes: 13 additions & 0 deletions lib/utils/downloadFile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// import 'dart:typed_data';
// import 'dart:io';

import 'package:http/http.dart';
// import 'package:path_provider/path_provider.dart';

Future<Stream<StreamedResponse>> downloadFile(String url) async {
var httpClient = Client();
var request = Request('GET', Uri.parse(url));
var response = httpClient.send(request);

return response.asStream();
}
85 changes: 85 additions & 0 deletions lib/utils/uploadFile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';

import 'package:http/http.dart';
import '../models/models.dart';

typedef void OnUploadProgressCallback(int sentBytes, int totalBytes);

Future<List<PapercupsAttachment>> uploadFile(Props p, String filePath,
{OnUploadProgressCallback? onUploadProgress}) async {
List<PapercupsAttachment>? pa = [];
try {
var uri = Uri.parse("https://" + p.baseUrl + "/api/upload");
final httpClient = HttpClient();
final request = await httpClient.postUrl(uri);
var client = MultipartRequest("POST", uri)
..fields['account_id'] = p.accountId
..files.add(await MultipartFile.fromPath('file', filePath));

var msStream = client.finalize();
var totalByteLength = client.contentLength;
request.contentLength = totalByteLength;

request.headers.set(
HttpHeaders.contentTypeHeader,
client.headers[HttpHeaders.contentTypeHeader] ?? '',
);
int byteCount = 0;

Stream<List<int>> streamUpload = msStream.transform(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
sink.add(data);

byteCount += data.length;

if (onUploadProgress != null) {
onUploadProgress(byteCount, totalByteLength);
}
},
handleError: (error, stack, sink) {
throw error;
},
handleDone: (sink) {
sink.close();
},
),
);

await request.addStream(streamUpload);

final httpResponse = await request.close();

var statusCode = httpResponse.statusCode;

if (statusCode ~/ 100 != 2) {
throw Exception(
'Error uploading file, Status code: ${httpResponse.statusCode}');
} else {
var body = await convertToString(httpResponse);
var data = jsonDecode(body)["data"];
pa.add(
PapercupsAttachment(
id: data["id"],
fileName: data["filename"],
fileUrl: data["file_url"],
contentType: data["content_type"],
),
);
}
} catch (e) {
throw (e);
}
return pa;
}

Future<String> convertToString(HttpClientResponse response) {
var completer = Completer<String>();
var contents = StringBuffer();
response.transform(utf8.decoder).listen((String data) {
contents.write(data);
}, onDone: () => completer.complete(contents.toString()));
return completer.future;
}
72 changes: 70 additions & 2 deletions lib/widgets/chat.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
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_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';

Expand All @@ -22,6 +27,7 @@ class ChatMessages extends StatelessWidget {
final String sendingText;
final String sentText;
final Color textColor;
final void Function(PapercupsMessage)? onMessageBubbleTap;

ChatMessages(
this.props,
Expand All @@ -32,7 +38,8 @@ class ChatMessages extends StatelessWidget {
this.timeagoLocale,
this.sendingText,
this.sentText,
this.textColor, {
this.textColor,
this.onMessageBubbleTap, {
Key? key,
}) : super(key: key);
@override
Expand Down Expand Up @@ -86,6 +93,7 @@ class ChatMessage extends StatefulWidget {
required this.sendingText,
required this.sentText,
required this.textColor,
this.onMessageBubbleTap,
}) : super(key: key);

final List<PapercupsMessage>? msgs;
Expand All @@ -98,6 +106,7 @@ class ChatMessage extends StatefulWidget {
final String sendingText;
final String sentText;
final Color textColor;
final void Function(PapercupsMessage)? onMessageBubbleTap;

@override
_ChatMessageState createState() => _ChatMessageState();
Expand All @@ -122,6 +131,52 @@ class _ChatMessageState extends State<ChatMessage> {
super.initState();
}

void _handleDownloadStream(Stream<StreamedResponse> resp,
{String? filename}) async {
String dir = (await getApplicationDocumentsDirectory()).path;

List<List<int>> chunks = [];
int downloaded = 0;

resp.listen((StreamedResponse r) {
r.stream.listen((List<int> chunk) {
Alert.show(
"downloadPercentage: ${downloaded / (r.contentLength ?? 1) * 100}",
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,
backgroundColor: Colors.green,
gravity: Alert.bottom,
duration: Alert.lengthLong,
);

File file = File('$dir/$filename');

print('location: $dir/$filename');

final Uint8List bytes = Uint8List(r.contentLength ?? 0);
int offset = 0;
for (List<int> 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) {
Expand All @@ -141,6 +196,9 @@ class _ChatMessageState extends State<ChatMessage> {
if (msg.userId != null) userSent = false;

var text = msg.body!;
if (msg.fileIds != null && msg.fileIds!.isNotEmpty) {
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;
Expand All @@ -167,10 +225,20 @@ class _ChatMessageState extends State<ChatMessage> {
});
if (!isLast && timer != null) timer!.cancel();
return GestureDetector(
onTap: () {
onTap: () async {
setState(() {
isTimeSentVisible = true;
});
if (widget.onMessageBubbleTap != null)
widget.onMessageBubbleTap!(msg);
else if ((msg.fileIds?.isNotEmpty ?? false)) {
Stream<StreamedResponse> resp =
await downloadFile(msg.attachments?.first.fileUrl ?? '');
_handleDownloadStream(
resp,
filename: msg.attachments?.first.fileName,
);
}
},
onLongPress: () {
HapticFeedback.vibrate();
Expand Down