|
|
import 'dart:async';
|
|
|
import 'dart:convert';
|
|
|
// import 'dart:io';
|
|
|
|
|
|
import 'package:bytedesk_kefu/blocs/message_bloc/bloc.dart';
|
|
|
import 'package:bytedesk_kefu/blocs/thread_bloc/bloc.dart';
|
|
|
import 'package:bytedesk_kefu/model/messageProvider.dart';
|
|
|
import 'package:bytedesk_kefu/model/model.dart';
|
|
|
import 'package:bytedesk_kefu/mqtt/bytedesk_mqtt.dart';
|
|
|
import 'package:bytedesk_kefu/ui/chat/widget/message_widget.dart';
|
|
|
import 'package:bytedesk_kefu/ui/leavemsg/provider/leavemsg_provider.dart';
|
|
|
import 'package:bytedesk_kefu/ui/widget/expanded_viewport.dart';
|
|
|
import 'package:bytedesk_kefu/ui/widget/image_choose_widget.dart';
|
|
|
import 'package:bytedesk_kefu/util/bytedesk_constants.dart';
|
|
|
import 'package:bytedesk_kefu/util/bytedesk_events.dart';
|
|
|
import 'package:bytedesk_kefu/util/bytedesk_utils.dart';
|
|
|
import 'package:bytedesk_kefu/util/bytedesk_uuid.dart';
|
|
|
import 'package:file_picker/file_picker.dart';
|
|
|
import 'package:sp_util/sp_util.dart';
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
// import 'package:flutter_video_compress/flutter_video_compress.dart';
|
|
|
// import 'package:flutter_image_compress/flutter_image_compress.dart';
|
|
|
import 'package:fluttertoast/fluttertoast.dart';
|
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
|
|
// import 'package:path_provider/path_provider.dart' as path_provider;
|
|
|
|
|
|
// TODO: 接通客服之前,在title显示loading
|
|
|
// 客服关闭会话,或者 自动关闭会话,则禁止继续发送消息
|
|
|
// 点击商品信息,回调接口-进入商品详情页面
|
|
|
// TODO: 右上角增加按钮回调入口,支持用户自定义按钮,进入店铺/学校详情页面
|
|
|
// TODO: 增加是否显示历史记录参数
|
|
|
// 系统消息居中显示
|
|
|
class ChatKFPage extends StatefulWidget {
|
|
|
//
|
|
|
final String? wid;
|
|
|
final String? aid;
|
|
|
final String? type;
|
|
|
final String? title;
|
|
|
final String? custom;
|
|
|
final String? postscript;
|
|
|
// 从历史会话或者点击通知栏进入
|
|
|
final bool? isThread;
|
|
|
final Thread? thread;
|
|
|
final ValueSetter<String>? customCallback;
|
|
|
//
|
|
|
ChatKFPage(
|
|
|
{Key? key,
|
|
|
this.wid,
|
|
|
this.aid,
|
|
|
this.type,
|
|
|
this.title,
|
|
|
this.custom,
|
|
|
this.postscript,
|
|
|
this.isThread,
|
|
|
this.thread,
|
|
|
this.customCallback})
|
|
|
: super(key: key);
|
|
|
//
|
|
|
@override
|
|
|
_ChatKFPageState createState() => _ChatKFPageState();
|
|
|
}
|
|
|
|
|
|
class _ChatKFPageState extends State<ChatKFPage>
|
|
|
with
|
|
|
AutomaticKeepAliveClientMixin<ChatKFPage>,
|
|
|
TickerProviderStateMixin,
|
|
|
WidgetsBindingObserver {
|
|
|
//
|
|
|
String? _title;
|
|
|
// 下拉刷新
|
|
|
RefreshController _refreshController = RefreshController();
|
|
|
// 输入文字
|
|
|
TextEditingController _textController = new TextEditingController();
|
|
|
// 滚动监听
|
|
|
ScrollController _scrollController = new ScrollController();
|
|
|
// 聊天记录本地存储
|
|
|
MessageProvider _messageProvider = new MessageProvider();
|
|
|
// 聊天记录内存存储
|
|
|
List<MessageWidget> _messages = <MessageWidget>[];
|
|
|
// 图片
|
|
|
ImagePicker _picker = ImagePicker();
|
|
|
// 长连接
|
|
|
BytedeskMqtt _bdMqtt = new BytedeskMqtt();
|
|
|
// 当前用户uid
|
|
|
String? _currentUid = SpUtil.getString(BytedeskConstants.uid);
|
|
|
String? _currentNickname = SpUtil.getString(BytedeskConstants.nickname);
|
|
|
String? _currentAvatar = SpUtil.getString(BytedeskConstants.avatar);
|
|
|
// 当前会话
|
|
|
Thread? _currentThread;
|
|
|
// 判断是否机器人对话状态
|
|
|
bool _isRobot = false;
|
|
|
// 分页加载聊天记录
|
|
|
int _page = 0;
|
|
|
int _size = 20;
|
|
|
// 延迟发送preview消息
|
|
|
Timer? _debounce;
|
|
|
// 定时拉取聊天记录
|
|
|
// Timer? _loadHistoryTimer;
|
|
|
// 视频压缩
|
|
|
// final _flutterVideoCompress = FlutterVideoCompress();
|
|
|
bool _isRequestingThread = true;
|
|
|
//
|
|
|
@override
|
|
|
void initState() {
|
|
|
// print('chat_kf_page init');
|
|
|
SpUtil.putBool(BytedeskConstants.isCurrentChatKfPage, true);
|
|
|
// 从历史会话或者顶部通知栏进入
|
|
|
if (widget.isThread! && widget.thread != null) {
|
|
|
_currentThread = widget.thread;
|
|
|
// FIXME: 在访客端-标题显示访客的名字,应该显示客服或技能组名字或固定写死
|
|
|
_title = widget.title!.trim().length > 0
|
|
|
? widget.title
|
|
|
: widget.thread!.nickname;
|
|
|
// _getMessages(_page, _size);
|
|
|
} else {
|
|
|
// 从请求客服页面进入
|
|
|
_title = widget.title;
|
|
|
}
|
|
|
WidgetsBinding.instance!.addObserver(this);
|
|
|
// 监听build完成,https://blog.csdn.net/baoolong/article/details/85097318
|
|
|
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
// print('addPostFrameCallback');
|
|
|
// });
|
|
|
// Fluttertoast.showToast(msg: "请求中, 请稍后...");
|
|
|
_listener();
|
|
|
super.initState();
|
|
|
// 定时拉取聊天记录 10s
|
|
|
// _loadHistoryTimer = Timer.periodic(Duration(seconds: 10), (timer) {
|
|
|
// // print('从服务器 load history');
|
|
|
// // TODO: 暂时禁用从服务器加载历史记录
|
|
|
// // BlocProvider.of<MessageBloc>(context)
|
|
|
// // ..add(LoadHistoryMessageEvent(uid: _currentUid, page: 0, size: 10));
|
|
|
// // 每隔 1 秒钟会调用一次,如果要结束调用
|
|
|
// // timer.cancel();
|
|
|
// });
|
|
|
}
|
|
|
|
|
|
//
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
super.build(context);
|
|
|
//
|
|
|
return Scaffold(
|
|
|
appBar: AppBar(
|
|
|
title: Text(_title ?? '请求中, 请稍后...'),
|
|
|
centerTitle: true,
|
|
|
elevation: 0,
|
|
|
actions: [
|
|
|
// TODO: 评价
|
|
|
// TODO: 常见问题
|
|
|
Visibility(
|
|
|
visible: _isRobot,
|
|
|
child: Align(
|
|
|
alignment: Alignment.centerRight,
|
|
|
child: Container(
|
|
|
padding: new EdgeInsets.only(right: 10),
|
|
|
width: 60,
|
|
|
child: InkWell(
|
|
|
onTap: () {
|
|
|
BlocProvider.of<ThreadBloc>(context)
|
|
|
..add(RequestAgentEvent(
|
|
|
wid: widget.wid,
|
|
|
aid: widget.aid,
|
|
|
type: widget.type));
|
|
|
},
|
|
|
child: Text(
|
|
|
'转人工',
|
|
|
// style: TextStyle(color: Colors.black),
|
|
|
),
|
|
|
))),
|
|
|
)
|
|
|
],
|
|
|
),
|
|
|
body: MultiBlocListener(
|
|
|
listeners: [
|
|
|
BlocListener<ThreadBloc, ThreadState>(
|
|
|
listener: (context, state) {
|
|
|
// 隐藏toast
|
|
|
// Fluttertoast.cancel();
|
|
|
if (state is RequestThreading) {
|
|
|
setState(() {
|
|
|
_isRequestingThread = true;
|
|
|
});
|
|
|
} else if (state is RequestThreadSuccess) {
|
|
|
setState(() {
|
|
|
_isRobot = false; // 需要,勿删
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
_isRequestingThread = false;
|
|
|
});
|
|
|
// 插入本地
|
|
|
// _messageProvider.insert(state.threadResult.msg!);
|
|
|
// TODO: 加载本地历史消息
|
|
|
_getMessages(_page, _size);
|
|
|
// _appendMessage(state.threadResult.msg!);
|
|
|
//
|
|
|
if (state.threadResult.statusCode == 200 ||
|
|
|
state.threadResult.statusCode == 201) {
|
|
|
print('创建新会话');
|
|
|
// 插入本地
|
|
|
// _messageProvider.insert(state.threadResult.msg!);
|
|
|
// TODO: 参考拼多多,在发送按钮上方显示pop商品信息,用户确认之后才会发送商品信息
|
|
|
// 发送商品信息
|
|
|
if (widget.custom != null &&
|
|
|
widget.custom!.trim().length > 0) {
|
|
|
_bdMqtt.sendCommodityMessage(
|
|
|
widget.custom!, _currentThread!);
|
|
|
}
|
|
|
// 发送附言消息
|
|
|
if (widget.postscript != null &&
|
|
|
widget.postscript!.trim().length > 0) {
|
|
|
_bdMqtt.sendTextMessage(
|
|
|
widget.postscript!, _currentThread!);
|
|
|
}
|
|
|
} else if (state.threadResult.statusCode == 202) {
|
|
|
print('提示排队中');
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// 加载本地历史消息
|
|
|
// _getMessages(_page, _size);
|
|
|
_appendMessage(state.threadResult.msg!);
|
|
|
// 发送商品信息
|
|
|
if (widget.custom != null &&
|
|
|
widget.custom!.trim().length > 0) {
|
|
|
_bdMqtt.sendCommodityMessage(
|
|
|
widget.custom!, _currentThread!);
|
|
|
}
|
|
|
// 发送附言消息
|
|
|
if (widget.postscript != null &&
|
|
|
widget.postscript!.trim().length > 0) {
|
|
|
_bdMqtt.sendTextMessage(
|
|
|
widget.postscript!, _currentThread!);
|
|
|
}
|
|
|
} else if (state.threadResult.statusCode == 203) {
|
|
|
print('当前非工作时间,请自助查询或留言');
|
|
|
setState(() {
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// TODO: 加载本地历史消息
|
|
|
// _getMessages(_page, _size);
|
|
|
_appendMessage(state.threadResult.msg!);
|
|
|
// 跳转留言页面,TODO: 关闭当前页面?
|
|
|
Navigator.of(context)
|
|
|
.push(new MaterialPageRoute(builder: (context) {
|
|
|
return new LeaveMsgProvider(
|
|
|
wid: this.widget.wid,
|
|
|
aid: this.widget.aid,
|
|
|
type: this.widget.type,
|
|
|
tip: state.threadResult.msg!.content);
|
|
|
}));
|
|
|
} else if (state.threadResult.statusCode == 204) {
|
|
|
print('当前无客服在线,请自助查询或留言');
|
|
|
setState(() {
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// TODO: 加载本地历史消息
|
|
|
// _getMessages(_page, _size);
|
|
|
_appendMessage(state.threadResult.msg!);
|
|
|
// 跳转留言页面, TODO: 关闭当前页面?
|
|
|
Navigator.of(context)
|
|
|
.push(new MaterialPageRoute(builder: (context) {
|
|
|
return new LeaveMsgProvider(
|
|
|
wid: this.widget.wid,
|
|
|
aid: this.widget.aid,
|
|
|
type: this.widget.type,
|
|
|
tip: state.threadResult.msg!.content);
|
|
|
}));
|
|
|
} else if (state.threadResult.statusCode == 205) {
|
|
|
print('咨询前问卷');
|
|
|
setState(() {
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// TODO: 加载本地历史消息
|
|
|
// _getMessages(_page, _size);
|
|
|
_appendMessage(state.threadResult.msg!);
|
|
|
//
|
|
|
} else if (state.threadResult.statusCode == 206) {
|
|
|
// print('返回机器人初始欢迎语 + 欢迎问题列表');
|
|
|
// TODO: 显示问题列表
|
|
|
setState(() {
|
|
|
_isRobot = true;
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// TODO: 加载本地历史消息
|
|
|
// _getMessages(_page, _size);
|
|
|
_appendMessage(state.threadResult.msg!);
|
|
|
//
|
|
|
} else if (state.threadResult.statusCode == -1) {
|
|
|
Fluttertoast.showToast(msg: "请求会话失败");
|
|
|
} else if (state.threadResult.statusCode == -2) {
|
|
|
Fluttertoast.showToast(msg: "siteId或者工作组id错误");
|
|
|
} else if (state.threadResult.statusCode == -3) {
|
|
|
Fluttertoast.showToast(msg: "您已经被禁言");
|
|
|
} else {
|
|
|
Fluttertoast.showToast(msg: "请求会话失败");
|
|
|
}
|
|
|
} else if (state is RequestThreadError) {
|
|
|
Fluttertoast.showToast(msg: "请求会话失败");
|
|
|
setState(() {
|
|
|
_isRequestingThread = false;
|
|
|
});
|
|
|
} else if (state is RequestAgentThreading) {
|
|
|
setState(() {
|
|
|
_isRequestingThread = true;
|
|
|
});
|
|
|
} else if (state is RequestAgentSuccess) {
|
|
|
// 请求人工客服,不管此工作组是否设置为默认机器人,只要有人工客服在线,则可以直接对接人工
|
|
|
setState(() {
|
|
|
_isRobot = false; // 需要,勿删
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
_isRequestingThread = false;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// _appendMessage(state.threadResult.msg!);
|
|
|
} else if (state is RequestAgentThreadError) {
|
|
|
Fluttertoast.showToast(msg: "请求会话失败");
|
|
|
setState(() {
|
|
|
_isRequestingThread = false;
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
),
|
|
|
BlocListener<MessageBloc, MessageState>(
|
|
|
listener: (context, state) {
|
|
|
// print('message state change');
|
|
|
if (state is ReceiveMessageState) {
|
|
|
// print('receive message:' + state.message!.content!);
|
|
|
} else if (state is MessageUpLoading) {
|
|
|
Fluttertoast.showToast(msg: '上传中...');
|
|
|
} else if (state is UploadImageSuccess) {
|
|
|
if (_bdMqtt.isConnected()) {
|
|
|
_bdMqtt.sendImageMessage(
|
|
|
state.uploadJsonResult.url!, _currentThread!);
|
|
|
} else {
|
|
|
sendImageMessageRest(state.uploadJsonResult.url!);
|
|
|
}
|
|
|
} else if (state is UploadVideoSuccess) {
|
|
|
_bdMqtt.sendVideoMessage(
|
|
|
state.uploadJsonResult.url!, _currentThread!);
|
|
|
// TODO: 调用http rest接口发送消息 sendImageRest
|
|
|
} else if (state is QueryAnswerSuccess) {
|
|
|
Message queryMessage = state.query!;
|
|
|
queryMessage.isSend = 1;
|
|
|
_messageProvider.insert(queryMessage);
|
|
|
_appendMessage(queryMessage);
|
|
|
//
|
|
|
_messageProvider.insert(state.answer!);
|
|
|
_appendMessage(state.answer!);
|
|
|
} else if (state is MessageAnswerSuccess) {
|
|
|
Message queryMessage = state.query!;
|
|
|
queryMessage.isSend = 1;
|
|
|
_messageProvider.insert(queryMessage);
|
|
|
_appendMessage(queryMessage);
|
|
|
//
|
|
|
if (state.query!.content!.contains('人工')) {
|
|
|
BlocProvider.of<ThreadBloc>(context)
|
|
|
..add(RequestAgentEvent(
|
|
|
wid: widget.wid,
|
|
|
aid: widget.aid,
|
|
|
type: widget.type));
|
|
|
} else {
|
|
|
_messageProvider.insert(state.answer!);
|
|
|
_appendMessage(state.answer!);
|
|
|
}
|
|
|
} else if (state is RateAnswerSuccess) {
|
|
|
// TODO:
|
|
|
} else if (state is LoadHistoryMessageSuccess) {
|
|
|
print('LoadHistoryMessageSuccess');
|
|
|
// 插入历史聊天记录
|
|
|
for (var i = 0; i < state.messageList!.length; i++) {
|
|
|
Message message = state.messageList![i];
|
|
|
_appendMessage(message);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
),
|
|
|
],
|
|
|
child: _isRequestingThread
|
|
|
? Container(
|
|
|
margin: EdgeInsets.only(top: 50),
|
|
|
alignment: Alignment.center,
|
|
|
child: Column(children: <Widget>[
|
|
|
CircularProgressIndicator(
|
|
|
strokeWidth: 2,
|
|
|
),
|
|
|
Text('会话请求中, 请稍后...')
|
|
|
]))
|
|
|
: Container(
|
|
|
alignment: Alignment.bottomCenter,
|
|
|
color: Color(0xFFDEEEEEE),
|
|
|
child: Column(
|
|
|
children: <Widget>[
|
|
|
// 参考pull_to_refresh库中 QQChatList例子
|
|
|
Expanded(
|
|
|
//
|
|
|
child: SmartRefresher(
|
|
|
enablePullDown: false,
|
|
|
onLoading: () async {
|
|
|
// print('TODO: 下拉刷新'); // 注意:方向跟默认是反着的
|
|
|
// await Future.delayed(Duration(milliseconds: 1000));
|
|
|
_getMessages(_page, _size);
|
|
|
setState(() {});
|
|
|
_refreshController.loadComplete();
|
|
|
},
|
|
|
footer: ClassicFooter(
|
|
|
loadStyle: LoadStyle.ShowWhenLoading,
|
|
|
),
|
|
|
enablePullUp: true,
|
|
|
//
|
|
|
child: Scrollable(
|
|
|
controller: _scrollController,
|
|
|
axisDirection: AxisDirection.up,
|
|
|
viewportBuilder: (context, offset) {
|
|
|
return ExpandedViewport(
|
|
|
offset: offset,
|
|
|
axisDirection: AxisDirection.up,
|
|
|
slivers: <Widget>[
|
|
|
SliverExpanded(),
|
|
|
SliverList(
|
|
|
delegate: SliverChildBuilderDelegate(
|
|
|
(c, i) => _messages[i],
|
|
|
childCount: _messages.length),
|
|
|
)
|
|
|
],
|
|
|
);
|
|
|
},
|
|
|
),
|
|
|
//
|
|
|
controller: _refreshController,
|
|
|
),
|
|
|
),
|
|
|
Divider(
|
|
|
height: 1.0,
|
|
|
),
|
|
|
Container(
|
|
|
decoration: BoxDecoration(
|
|
|
color: Theme.of(context).cardColor,
|
|
|
),
|
|
|
child: _textComposerWidget(),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
)));
|
|
|
}
|
|
|
|
|
|
//
|
|
|
Widget _textComposerWidget() {
|
|
|
return IconTheme(
|
|
|
data: IconThemeData(color: Colors.blue),
|
|
|
child: Container(
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
|
height: 54,
|
|
|
child: Row(
|
|
|
children: <Widget>[
|
|
|
Container(
|
|
|
// margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
|
|
child: new IconButton(
|
|
|
icon: new Icon(Icons.add),
|
|
|
onPressed: () {
|
|
|
// _getImage();
|
|
|
showModalBottomSheet(
|
|
|
context: context,
|
|
|
builder: (context) {
|
|
|
return ImageChooseWidget(pickImageCallBack: () {
|
|
|
_pickImage();
|
|
|
}, takeImageCallBack: () {
|
|
|
_takeImage();
|
|
|
}, pickVideoCallBack: () {
|
|
|
_pickVideo();
|
|
|
}, captureVideoCallBack: () {
|
|
|
_captureVideo();
|
|
|
});
|
|
|
});
|
|
|
},
|
|
|
),
|
|
|
),
|
|
|
Flexible(
|
|
|
child: TextField(
|
|
|
onChanged: (value) {
|
|
|
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
|
|
// 积累500毫秒,再发送。否则发送过于频繁
|
|
|
_debounce = Timer(const Duration(milliseconds: 500), () {
|
|
|
print('send preview $value');
|
|
|
// 发送预知消息
|
|
|
if (value.trim().length > 0) {
|
|
|
_bdMqtt.sendPreviewMessage(value, _currentThread!);
|
|
|
} else {
|
|
|
_bdMqtt.sendPreviewMessage('', _currentThread!);
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
decoration: InputDecoration.collapsed(hintText: "输入内容..."),
|
|
|
controller: _textController,
|
|
|
onSubmitted: _handleSubmitted,
|
|
|
),
|
|
|
),
|
|
|
Container(
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
|
|
child: IconButton(
|
|
|
icon: Icon(Icons.send),
|
|
|
onPressed: () => _handleSubmitted(_textController.text),
|
|
|
),
|
|
|
)
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
//
|
|
|
@override
|
|
|
bool get wantKeepAlive => true;
|
|
|
|
|
|
//
|
|
|
void _handleSubmitted(String? text) {
|
|
|
_textController.clear();
|
|
|
// 内容为空,直接返回
|
|
|
if (text!.trim().length == 0) {
|
|
|
return;
|
|
|
}
|
|
|
//
|
|
|
if (_isRobot) {
|
|
|
// 请求机器人答案
|
|
|
BlocProvider.of<MessageBloc>(context)
|
|
|
..add(MessageAnswerEvent(
|
|
|
type: widget.type,
|
|
|
wid: widget.wid,
|
|
|
aid: widget.aid,
|
|
|
content: text));
|
|
|
} else if (_bdMqtt.isConnected()) {
|
|
|
if (_currentThread == null) {
|
|
|
Fluttertoast.showToast(msg: '请求客服中, 请稍后...');
|
|
|
return;
|
|
|
}
|
|
|
// 长连接正常情况下,调用长连接接口
|
|
|
_bdMqtt.sendTextMessage(text, _currentThread!);
|
|
|
} else {
|
|
|
print('长连接断开的情况下,调用rest接口');
|
|
|
sendTextMessageRest(text);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// http rest 接口发生文本消息
|
|
|
void sendTextMessageRest(String text) {
|
|
|
//
|
|
|
String? mid = BytedeskUuid.generateV4();
|
|
|
String? timestamp = BytedeskUtils.formatedDateNow();
|
|
|
String? client = BytedeskUtils.getClient();
|
|
|
String? type = BytedeskConstants.MESSAGE_TYPE_TEXT;
|
|
|
//
|
|
|
var jsonContent = {
|
|
|
"mid": mid,
|
|
|
"timestamp": timestamp,
|
|
|
"client": client,
|
|
|
"version": "1",
|
|
|
"type": type,
|
|
|
"user": {
|
|
|
"uid": this._currentUid,
|
|
|
"nickname": this._currentNickname,
|
|
|
"avatar": this._currentAvatar,
|
|
|
"extra": {"agent": false}
|
|
|
},
|
|
|
"text": {"content": text},
|
|
|
"thread": {
|
|
|
"tid": this._currentThread!.tid,
|
|
|
"type": this._currentThread!.type,
|
|
|
"content": text,
|
|
|
"nickname": this._currentThread!.nickname,
|
|
|
"avatar": this._currentThread!.avatar,
|
|
|
"topic": this._currentThread!.topic,
|
|
|
"timestamp": timestamp,
|
|
|
"unreadCount": 0
|
|
|
}
|
|
|
};
|
|
|
String? jsonString = json.encode(jsonContent);
|
|
|
BlocProvider.of<MessageBloc>(context)
|
|
|
..add(SendMessageRestEvent(json: jsonString));
|
|
|
// 暂时没有将插入本地函数独立出来,暂时
|
|
|
Message message = new Message();
|
|
|
message.mid = mid;
|
|
|
message.type = type;
|
|
|
message.timestamp = timestamp;
|
|
|
message.client = client;
|
|
|
message.avatar = _currentAvatar;
|
|
|
message.topic = this._currentThread!.topic;
|
|
|
message.status = BytedeskConstants.MESSAGE_STATUS_SENDING;
|
|
|
message.isSend = 1;
|
|
|
message.currentUid = this._currentUid;
|
|
|
message.answersJson = '';
|
|
|
message.thread = this._currentThread;
|
|
|
message.user = User(
|
|
|
uid: this._currentUid,
|
|
|
avatar: this._currentAvatar,
|
|
|
nickname: this._currentNickname);
|
|
|
//
|
|
|
message.content = text;
|
|
|
// 插入本地数据库
|
|
|
_messageProvider.insert(message);
|
|
|
}
|
|
|
|
|
|
// http rest 接口发送图片消息
|
|
|
void sendImageMessageRest(String imageUrl) {
|
|
|
//
|
|
|
String? mid = BytedeskUuid.generateV4();
|
|
|
String? timestamp = BytedeskUtils.formatedDateNow();
|
|
|
String? client = BytedeskUtils.getClient();
|
|
|
String? type = BytedeskConstants.MESSAGE_TYPE_IMAGE;
|
|
|
//
|
|
|
var jsonContent = {
|
|
|
"mid": mid,
|
|
|
"timestamp": timestamp,
|
|
|
"client": client,
|
|
|
"version": "1",
|
|
|
"type": type,
|
|
|
"user": {
|
|
|
"uid": this._currentUid,
|
|
|
"nickname": this._currentNickname,
|
|
|
"avatar": this._currentAvatar,
|
|
|
"extra": {"agent": false}
|
|
|
},
|
|
|
"image": {"imageUrl": imageUrl},
|
|
|
"thread": {
|
|
|
"tid": this._currentThread!.tid,
|
|
|
"type": this._currentThread!.type,
|
|
|
"content": "[图片]",
|
|
|
"nickname": this._currentThread!.nickname,
|
|
|
"avatar": this._currentThread!.avatar,
|
|
|
"topic": this._currentThread!.topic,
|
|
|
"timestamp": timestamp,
|
|
|
"unreadCount": 0
|
|
|
}
|
|
|
};
|
|
|
String? jsonString = json.encode(jsonContent);
|
|
|
BlocProvider.of<MessageBloc>(context)
|
|
|
..add(SendMessageRestEvent(json: jsonString));
|
|
|
// 暂时没有将插入本地函数独立出来,暂时
|
|
|
Message message = new Message();
|
|
|
message.mid = mid;
|
|
|
message.type = type;
|
|
|
message.timestamp = timestamp;
|
|
|
// message.client = client;
|
|
|
message.avatar = _currentAvatar;
|
|
|
message.topic = this._currentThread!.topic;
|
|
|
message.status = BytedeskConstants.MESSAGE_STATUS_SENDING;
|
|
|
message.isSend = 1;
|
|
|
message.currentUid = this._currentUid;
|
|
|
message.answersJson = '';
|
|
|
message.thread = this._currentThread;
|
|
|
message.user = User(
|
|
|
uid: this._currentUid,
|
|
|
avatar: this._currentAvatar,
|
|
|
nickname: this._currentNickname);
|
|
|
//
|
|
|
message.imageUrl = imageUrl;
|
|
|
// 插入本地数据库
|
|
|
_messageProvider.insert(message);
|
|
|
}
|
|
|
|
|
|
//
|
|
|
_listener() {
|
|
|
// 更新消息状态
|
|
|
bytedeskEventBus.on<ReceiveMessageReceiptEventBus>().listen((event) {
|
|
|
// print('更新状态:' + event.mid + '-' + event.status);
|
|
|
if (this.mounted) {
|
|
|
// 更新界面
|
|
|
for (var i = 0; i < _messages.length; i++) {
|
|
|
MessageWidget messageWidget = _messages[i];
|
|
|
if (messageWidget.message!.mid == event.mid &&
|
|
|
_messages[i].message!.status !=
|
|
|
BytedeskConstants.MESSAGE_STATUS_READ) {
|
|
|
// print('do update status:' + messageWidget.message!.mid!);
|
|
|
// setState(() {
|
|
|
// _messages[i].message!.status = event.status; // 不更新
|
|
|
// });
|
|
|
// 必须重新创建一个messageWidget才会更新
|
|
|
Message message = messageWidget.message!;
|
|
|
message.status = event.status;
|
|
|
MessageWidget messageWidget2 = new MessageWidget(
|
|
|
message: message,
|
|
|
customCallback: widget.customCallback,
|
|
|
animationController: new AnimationController(
|
|
|
vsync: this, duration: Duration(milliseconds: 500)));
|
|
|
setState(() {
|
|
|
_messages[i] = messageWidget2;
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
bytedeskEventBus.on<ReceiveMessagePreviewEventBus>().listen((event) {
|
|
|
// print('消息预知');
|
|
|
if (this.mounted) {
|
|
|
setState(() {
|
|
|
// TODO: 国际化,支持英文
|
|
|
_title = '对方正在输入';
|
|
|
});
|
|
|
}
|
|
|
// 还原title
|
|
|
Timer(
|
|
|
Duration(seconds: 3),
|
|
|
() {
|
|
|
// print('timer');
|
|
|
if (this.mounted) {
|
|
|
setState(() {
|
|
|
_title = widget.title;
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
);
|
|
|
});
|
|
|
// 接收到新消息
|
|
|
bytedeskEventBus.on<ReceiveMessageEventBus>().listen((event) {
|
|
|
// print('receive message:' + event.message!.content);
|
|
|
if (_currentThread != null &&
|
|
|
(event.message.thread!.topic != _currentThread!.topic)) {
|
|
|
return;
|
|
|
}
|
|
|
// 非自己发送的,发送已读回执
|
|
|
if (event.message.isSend == 0) {
|
|
|
_bdMqtt.sendReceiptReadMessage(
|
|
|
event.message.mid!, event.message.thread!);
|
|
|
}
|
|
|
//
|
|
|
if (this.mounted) {
|
|
|
// 界面显示
|
|
|
MessageWidget messageWidget = new MessageWidget(
|
|
|
message: event.message,
|
|
|
customCallback: widget.customCallback,
|
|
|
animationController: new AnimationController(
|
|
|
vsync: this, duration: Duration(milliseconds: 500)));
|
|
|
setState(() {
|
|
|
_messages.insert(0, messageWidget);
|
|
|
// _messages.add(messageWidget);
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
// 删除消息
|
|
|
bytedeskEventBus.on<DeleteMessageEventBus>().listen((event) {
|
|
|
//
|
|
|
if (this.mounted) {
|
|
|
// 从sqlite中删除
|
|
|
_messageProvider.delete(event.mid);
|
|
|
// 更新界面
|
|
|
setState(() {
|
|
|
_messages.removeWhere((element) => element.message!.mid == event.mid);
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
// 查询机器人消息
|
|
|
bytedeskEventBus.on<QueryAnswerEventBus>().listen((event) {
|
|
|
if (this.mounted) {
|
|
|
// print('aid ${event.aid}, question ${event.question}, answer ${event.answer}');
|
|
|
// 可以直接将问题和答案插入本地,并显示,但为了服务器保存查询记录,特将请求发送给服务器
|
|
|
BlocProvider.of<MessageBloc>(context)
|
|
|
..add(QueryAnswerEvent(
|
|
|
tid: _currentThread!.tid,
|
|
|
aid: event.aid,
|
|
|
));
|
|
|
}
|
|
|
});
|
|
|
// 点击机器人消息 ‘人工客服’
|
|
|
bytedeskEventBus.on<RequestAgentThreadEventBus>().listen((event) {
|
|
|
if (this.mounted) {
|
|
|
BlocProvider.of<ThreadBloc>(context)
|
|
|
..add(RequestAgentEvent(
|
|
|
wid: widget.wid, aid: widget.aid, type: widget.type));
|
|
|
}
|
|
|
});
|
|
|
// 滚动监听, https://learnku.com/articles/30338
|
|
|
_scrollController.addListener(() {
|
|
|
// 隐藏软键盘
|
|
|
FocusScope.of(context).requestFocus(FocusNode());
|
|
|
// 如果滑动到底部
|
|
|
// if (_scrollController.position.pixels ==
|
|
|
// _scrollController.position.maxScrollExtent) {
|
|
|
// print('已经到底了');
|
|
|
// }
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 选择图片
|
|
|
Future<void> _pickImage() async {
|
|
|
try {
|
|
|
//
|
|
|
XFile? pickedFile = await _picker.pickImage(
|
|
|
source: ImageSource.gallery, maxWidth: 800, imageQuality: 95);
|
|
|
//
|
|
|
if (pickedFile != null) {
|
|
|
print('pick image path: ${pickedFile.path}');
|
|
|
// TODO: 将图片显示到对话消息中
|
|
|
// TODO: 显示处理中loading
|
|
|
// 压缩
|
|
|
// final dir = await path_provider.getTemporaryDirectory();
|
|
|
// final targetPath = dir.absolute.path +
|
|
|
// "/" +
|
|
|
// BytedeskUtils.currentTimeMillis().toString() +
|
|
|
// ".jpg";
|
|
|
// print('targetPath: $targetPath');
|
|
|
// await BytedeskUtils.compressImage(File(pickedFile.path), targetPath);
|
|
|
// // 上传压缩后图片
|
|
|
// BlocProvider.of<MessageBloc>(context)
|
|
|
// ..add(UploadImageEvent(filePath: targetPath));
|
|
|
//
|
|
|
BlocProvider.of<MessageBloc>(context)
|
|
|
..add(UploadImageEvent(filePath: pickedFile.path));
|
|
|
} else {
|
|
|
Fluttertoast.showToast(msg: '未选取图片');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
print('pick image error ${e.toString()}');
|
|
|
Fluttertoast.showToast(msg: "未选取图片");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 拍照
|
|
|
Future<void> _takeImage() async {
|
|
|
try {
|
|
|
XFile? pickedFile = await _picker.pickImage(
|
|
|
source: ImageSource.camera, maxWidth: 800, imageQuality: 95);
|
|
|
//
|
|
|
if (pickedFile != null) {
|
|
|
print('take image path: ${pickedFile.path}');
|
|
|
// TODO: 将图片显示到对话消息中
|
|
|
// TODO: 显示处理中loading
|
|
|
// 压缩
|
|
|
// final dir = await path_provider.getTemporaryDirectory();
|
|
|
// final targetPath = dir.absolute.path +
|
|
|
// "/" +
|
|
|
// BytedeskUtils.currentTimeMillis().toString() +
|
|
|
// ".jpg";
|
|
|
// print('targetPath: $targetPath');
|
|
|
// await BytedeskUtils.compressImage(File(pickedFile.path), targetPath);
|
|
|
// // 上传压缩后图片
|
|
|
// BlocProvider.of<MessageBloc>(context)
|
|
|
// ..add(UploadImageEvent(filePath: targetPath));
|
|
|
//
|
|
|
BlocProvider.of<MessageBloc>(context)
|
|
|
..add(UploadImageEvent(filePath: pickedFile.path));
|
|
|
} else {
|
|
|
Fluttertoast.showToast(msg: '未拍照');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
print('take image error ${e.toString()}');
|
|
|
Fluttertoast.showToast(msg: "未选取图片");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 上传视频
|
|
|
// FIXME: image_picker有bug,选择视频后缀为.jpg
|
|
|
// 手机可以播放,但chrome无法播放
|
|
|
Future<void> _pickVideo() async {
|
|
|
try {
|
|
|
// final PickedFile videoFile = await _picker.getVideo(
|
|
|
// source: ImageSource.gallery, maxDuration: const Duration(seconds: 10));
|
|
|
// print('pick video path: ${videoFile.path}');
|
|
|
// if (videoFile != null) {
|
|
|
// BlocProvider.of<MessageBloc>(context)
|
|
|
// ..add(UploadVideoEvent(filePath: videoFile.path));
|
|
|
// } else {
|
|
|
// Fluttertoast.showToast(msg: '未选取视频');
|
|
|
// }
|
|
|
// 使用file_picker替换image_picker
|
|
|
List<PlatformFile>? _paths = (await FilePicker.platform.pickFiles(
|
|
|
type: FileType.video,
|
|
|
allowMultiple: false,
|
|
|
allowedExtensions: [],
|
|
|
))
|
|
|
?.files;
|
|
|
if (_paths!.length > 0) {
|
|
|
// TODO: 将视频显示到对话消息中
|
|
|
// TODO: 显示处理中loading
|
|
|
// 压缩
|
|
|
// final info = await _flutterVideoCompress.compressVideo(
|
|
|
// _paths[0].path,
|
|
|
// quality:
|
|
|
// VideoQuality.LowQuality, // default(VideoQuality.DefaultQuality)
|
|
|
// deleteOrigin: false, // default(false)
|
|
|
// );
|
|
|
// // debugPrint(info.toJson().toString());
|
|
|
// String? afterPath = info.toJson()['path'];
|
|
|
// // print('video path: ${_paths[0].path}, compress path: $afterPath');
|
|
|
// // 上传
|
|
|
// BlocProvider.of<MessageBloc>(context)
|
|
|
// ..add(UploadVideoEvent(filePath: afterPath));
|
|
|
// 压缩后上传
|
|
|
BlocProvider.of<MessageBloc>(context)
|
|
|
..add(UploadVideoEvent(filePath: _paths[0].path));
|
|
|
}
|
|
|
} catch (e) {
|
|
|
print('pick video error ${e.toString()}');
|
|
|
Fluttertoast.showToast(msg: "未选取视频");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 录制视频
|
|
|
Future<void> _captureVideo() async {
|
|
|
try {
|
|
|
XFile? pickedFile = await _picker.pickVideo(
|
|
|
source: ImageSource.camera, maxDuration: const Duration(seconds: 10));
|
|
|
//
|
|
|
if (pickedFile != null) {
|
|
|
print('take video path: ${pickedFile.path}');
|
|
|
// TODO: 将图片显示到对话消息中
|
|
|
// TODO: 显示处理中loading
|
|
|
// 压缩
|
|
|
// final info = await _flutterVideoCompress.compressVideo(
|
|
|
// pickedFile.path,
|
|
|
// quality:
|
|
|
// VideoQuality.LowQuality, // default(VideoQuality.DefaultQuality)
|
|
|
// deleteOrigin: false, // default(false)
|
|
|
// );
|
|
|
// // debugPrint(info.toJson().toString());
|
|
|
// String? afterPath = info.toJson()['path'];
|
|
|
// // print('video path: ${pickedFile.path}, compress path: $afterPath');
|
|
|
// // 上传
|
|
|
// BlocProvider.of<MessageBloc>(context)
|
|
|
// ..add(UploadVideoEvent(filePath: afterPath));
|
|
|
//
|
|
|
BlocProvider.of<MessageBloc>(context)
|
|
|
..add(UploadVideoEvent(filePath: pickedFile.path));
|
|
|
} else {
|
|
|
Fluttertoast.showToast(msg: '未录制视频');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
print('take video error ${e.toString()}');
|
|
|
Fluttertoast.showToast(msg: "未录制视频");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 加载更多聊天记录
|
|
|
// Future<void> _loadMoreMessages() async {
|
|
|
// print('load more');
|
|
|
// // TODO: 从服务器加载
|
|
|
// _getMessages(_page, _size);
|
|
|
// }
|
|
|
|
|
|
// 分页加载本地历史聊天记录
|
|
|
// TODO: 从服务器加载聊天记录
|
|
|
// FIXME: 消息排序错乱
|
|
|
Future<Null> _getMessages(int page, int size) async {
|
|
|
// BlocProvider.of<MessageBloc>(context)
|
|
|
// ..add(LoadHistoryMessageEvent(uid: _currentUid, page: page, size: size));
|
|
|
//
|
|
|
List<Message> messageList = await _messageProvider.getTopicMessages(
|
|
|
_currentThread!.topic, _currentUid, page, size);
|
|
|
// print(messageList.length);
|
|
|
int length = messageList.length;
|
|
|
for (var i = 0; i < length; i++) {
|
|
|
Message message = messageList[i];
|
|
|
if (message.type ==
|
|
|
BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_FORM_REQUEST ||
|
|
|
message.type ==
|
|
|
BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_FORM_RESULT) {
|
|
|
// 暂时忽略表单消息
|
|
|
} else if (message.type ==
|
|
|
BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_THREAD_REENTRY) {
|
|
|
// 连续的 ‘继续会话’ 消息,只显示最后一条
|
|
|
if (i + 1 < length) {
|
|
|
var nextmsg = messageList[i + 1];
|
|
|
if (nextmsg.type ==
|
|
|
BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_THREAD_REENTRY) {
|
|
|
continue;
|
|
|
} else {
|
|
|
pushToMessageArray(message);
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
pushToMessageArray(message);
|
|
|
}
|
|
|
}
|
|
|
//
|
|
|
_page += 1;
|
|
|
}
|
|
|
|
|
|
void pushToMessageArray(Message message) {
|
|
|
if (this.mounted) {
|
|
|
MessageWidget messageWidget = new MessageWidget(
|
|
|
message: message,
|
|
|
customCallback: widget.customCallback,
|
|
|
animationController: new AnimationController(
|
|
|
vsync: this, duration: Duration(milliseconds: 500)));
|
|
|
setState(() {
|
|
|
_messages.add(messageWidget);
|
|
|
_messages.sort((a, b) {
|
|
|
return b.message!.timestamp!.compareTo(a.message!.timestamp!);
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
if (message.status != BytedeskConstants.MESSAGE_STATUS_READ) {
|
|
|
// 发送已读回执
|
|
|
if (message.isSend == 0) {
|
|
|
_bdMqtt.sendReceiptReadMessage(message.mid!, _currentThread!);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Future<Null> _appendMessage(Message message) async {
|
|
|
// print('append:' + message.mid! + 'content:' + message.content!);
|
|
|
bool contains = false;
|
|
|
for (var i = 0; i < _messages.length; i++) {
|
|
|
Message? element = _messages[i].message;
|
|
|
if (element!.mid == message.mid) {
|
|
|
contains = true;
|
|
|
// 更新消息状态
|
|
|
_messageProvider.update(element.mid, message.status);
|
|
|
}
|
|
|
}
|
|
|
if (!contains) {
|
|
|
// _messageProvider.insert(message); // 造成重复插入
|
|
|
MessageWidget messageWidget = new MessageWidget(
|
|
|
message: message,
|
|
|
customCallback: widget.customCallback,
|
|
|
animationController: new AnimationController(
|
|
|
vsync: this, duration: Duration(milliseconds: 500)));
|
|
|
if (this.mounted) {
|
|
|
setState(() {
|
|
|
_messages.insert(0, messageWidget);
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void scrollToBottom() {
|
|
|
// After 1 second, it takes you to the bottom of the ListView
|
|
|
// Timer(
|
|
|
// Duration(seconds: 1),
|
|
|
// () => _scrollController.jumpTo(_scrollController.position.maxScrollExtent),
|
|
|
// );
|
|
|
_scrollController.animateTo(
|
|
|
_scrollController.position.maxScrollExtent,
|
|
|
duration: Duration(seconds: 1),
|
|
|
curve: Curves.fastOutSlowIn,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
|
// print("didChangeAppLifecycleState:" + state.toString());
|
|
|
switch (state) {
|
|
|
case AppLifecycleState.inactive: // 处于这种状态的应用程序应该假设它们可能在任何时候暂停。
|
|
|
break;
|
|
|
case AppLifecycleState.paused: // 应用程序不可见,后台
|
|
|
break;
|
|
|
case AppLifecycleState.resumed: // 应用程序可见,前台
|
|
|
// APP切换到前台之后,重连
|
|
|
// BytedeskUtils.mqttReConnect();
|
|
|
// TODO: 拉取离线消息
|
|
|
break;
|
|
|
case AppLifecycleState.detached: // 申请将暂时暂停
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
void dispose() {
|
|
|
// print('chat_kf_page dispose');
|
|
|
SpUtil.putBool(BytedeskConstants.isCurrentChatKfPage, false);
|
|
|
WidgetsBinding.instance!.removeObserver(this);
|
|
|
_debounce?.cancel();
|
|
|
// _loadHistoryTimer?.cancel();
|
|
|
super.dispose();
|
|
|
}
|
|
|
}
|