|
|
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/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:bytedesk_kefu/util/bytedesk_utils.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: 右上角增加按钮回调入口,支持用户自定义按钮,进入店铺/学校详情页面
|
|
|
// 系统消息居中显示
|
|
|
class ChatIMPage 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;
|
|
|
//
|
|
|
ChatIMPage(
|
|
|
{Key? key,
|
|
|
this.wid,
|
|
|
this.aid,
|
|
|
this.type,
|
|
|
this.title,
|
|
|
this.custom,
|
|
|
this.postscript,
|
|
|
this.isThread,
|
|
|
this.thread,
|
|
|
this.customCallback})
|
|
|
: super(key: key);
|
|
|
//
|
|
|
@override
|
|
|
_ChatIMPageState createState() => _ChatIMPageState();
|
|
|
}
|
|
|
|
|
|
class _ChatIMPageState extends State<ChatIMPage>
|
|
|
with
|
|
|
AutomaticKeepAliveClientMixin<ChatIMPage>,
|
|
|
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();
|
|
|
//
|
|
|
@override
|
|
|
void initState() {
|
|
|
// BytedeskUtils.printLog('chat_kf_page init');
|
|
|
SpUtil.putBool(BytedeskConstants.isCurrentChatKfPage, true);
|
|
|
// 从历史会话或者顶部通知栏进入
|
|
|
if (widget.isThread! && widget.thread != null) {
|
|
|
_currentThread = widget.thread;
|
|
|
_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((_) {
|
|
|
// BytedeskUtils.printLog('addPostFrameCallback');
|
|
|
// });
|
|
|
// Fluttertoast.showToast(msg: "请求中, 请稍后...");
|
|
|
_listener();
|
|
|
super.initState();
|
|
|
// 定时拉取聊天记录 10s
|
|
|
_loadHistoryTimer = Timer.periodic(Duration(seconds: 10), (timer) {
|
|
|
// BytedeskUtils.printLog('从服务器 load history');
|
|
|
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 RequestThreadSuccess) {
|
|
|
setState(() {
|
|
|
_isRobot = false; // 需要,勿删
|
|
|
_currentThread = state.threadResult.msg!.thread!;
|
|
|
});
|
|
|
// 插入本地
|
|
|
// _messageProvider.insert(state.threadResult.msg!);
|
|
|
// 加载本地历史消息
|
|
|
_getMessages(_page, _size);
|
|
|
// _appendMessage(state.threadResult.msg!);
|
|
|
//
|
|
|
if (state.threadResult.statusCode == 200 ||
|
|
|
state.threadResult.statusCode == 201) {
|
|
|
BytedeskUtils.printLog('创建新会话');
|
|
|
// 插入本地
|
|
|
// _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) {
|
|
|
BytedeskUtils.printLog('提示排队中');
|
|
|
// 插入本地
|
|
|
_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) {
|
|
|
BytedeskUtils.printLog('当前非工作时间,请自助查询或留言');
|
|
|
// TODO: 显示留言页面
|
|
|
setState(() {
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// 加载本地历史消息
|
|
|
_getMessages(_page, _size);
|
|
|
_appendMessage(state.threadResult.msg!);
|
|
|
} else if (state.threadResult.statusCode == 204) {
|
|
|
BytedeskUtils.printLog('当前无客服在线,请自助查询或留言');
|
|
|
// TODO: 显示留言页面
|
|
|
setState(() {
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// 加载本地历史消息
|
|
|
_getMessages(_page, _size);
|
|
|
_appendMessage(state.threadResult.msg!);
|
|
|
} else if (state.threadResult.statusCode == 205) {
|
|
|
BytedeskUtils.printLog('咨询前问卷');
|
|
|
setState(() {
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// 加载本地历史消息
|
|
|
_getMessages(_page, _size);
|
|
|
_appendMessage(state.threadResult.msg!);
|
|
|
//
|
|
|
} else if (state.threadResult.statusCode == 206) {
|
|
|
BytedeskUtils.printLog('返回机器人初始欢迎语 + 欢迎问题列表');
|
|
|
// TODO: 显示问题列表
|
|
|
setState(() {
|
|
|
_isRobot = true;
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// 加载本地历史消息
|
|
|
_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 RequestAgentSuccess) {
|
|
|
// 请求人工客服,不管此工作组是否设置为默认机器人,只要有人工客服在线,则可以直接对接人工
|
|
|
setState(() {
|
|
|
_isRobot = false; // 需要,勿删
|
|
|
_currentThread = state.threadResult.msg!.thread;
|
|
|
});
|
|
|
// 插入本地
|
|
|
_messageProvider.insert(state.threadResult.msg!);
|
|
|
// _appendMessage(state.threadResult.msg!);
|
|
|
}
|
|
|
},
|
|
|
),
|
|
|
BlocListener<MessageBloc, MessageState>(
|
|
|
listener: (context, state) {
|
|
|
// BytedeskUtils.printLog('message state change');
|
|
|
if (state is ReceiveMessageState) {
|
|
|
BytedeskUtils.printLog(
|
|
|
'receive message:' + state.message!.content!);
|
|
|
} else if (state is UploadImageSuccess) {
|
|
|
_bdMqtt.sendImageMessage(
|
|
|
state.uploadJsonResult.url!, _currentThread!);
|
|
|
} else if (state is UploadVideoSuccess) {
|
|
|
_bdMqtt.sendVideoMessage(
|
|
|
state.uploadJsonResult.url!, _currentThread!);
|
|
|
} 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) {
|
|
|
// TODO: 插入历史聊天记录
|
|
|
// BytedeskUtils.printLog('history ${state.messageList!.length}');
|
|
|
for (var i = 0; i < state.messageList!.length; i++) {
|
|
|
// Message message = state.messageList[i];
|
|
|
// _appendMessage(message);
|
|
|
}
|
|
|
} else if (state is LoadTopicMessageSuccess) {
|
|
|
// TODO: 插入历史聊天记录
|
|
|
BytedeskUtils.printLog(
|
|
|
'topic history ${state.messageList!.length}');
|
|
|
for (var i = 0; i < state.messageList!.length; i++) {
|
|
|
// Message message = state.messageList[i];
|
|
|
// _appendMessage(message);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
),
|
|
|
],
|
|
|
child: Container(
|
|
|
alignment: Alignment.bottomCenter,
|
|
|
color: Color(0xFFDEEEEEE),
|
|
|
child: Column(
|
|
|
children: <Widget>[
|
|
|
// Expanded(
|
|
|
// child: RefreshIndicator(
|
|
|
// child: ListView.builder(
|
|
|
// padding: EdgeInsets.all(8.0),
|
|
|
// // reverse: true,
|
|
|
// controller: _scrollController,
|
|
|
// itemCount: _messages.length,
|
|
|
// itemBuilder: (_, int index) => _messages[index],
|
|
|
// ),
|
|
|
// onRefresh: _loadMoreMessages,
|
|
|
// ),
|
|
|
// ),
|
|
|
// Expanded(
|
|
|
// child: ListView.builder(
|
|
|
// padding: new EdgeInsets.all(8.0),
|
|
|
// reverse: true,
|
|
|
// shrinkWrap: true,
|
|
|
// controller: _scrollController,
|
|
|
// itemBuilder: (_, int index) => _messages[index],
|
|
|
// itemCount: _messages.length,
|
|
|
// ),
|
|
|
// ),
|
|
|
// 参考pull_to_refresh库中 QQChatList例子
|
|
|
Expanded(
|
|
|
child: SmartRefresher(
|
|
|
enablePullDown: false,
|
|
|
onLoading: () async {
|
|
|
BytedeskUtils.printLog('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), () {
|
|
|
BytedeskUtils.printLog('send preview $value');
|
|
|
// 发送预知消息 value != null &&
|
|
|
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 {
|
|
|
BytedeskUtils.printLog('长连接断开的情况下,调用rest接口');
|
|
|
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
|
|
|
},
|
|
|
"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;
|
|
|
// 插入本地数据库
|
|
|
// if (_messageProvider != null) {
|
|
|
_messageProvider.insert(message);
|
|
|
// }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//
|
|
|
_listener() {
|
|
|
// 更新消息状态
|
|
|
bytedeskEventBus.on<ReceiveMessageReceiptEventBus>().listen((event) {
|
|
|
// BytedeskUtils.printLog('更新状态:' + event.status);
|
|
|
if (!this.mounted) {
|
|
|
return;
|
|
|
}
|
|
|
// 更新界面, FIXME: 只有插入新消息,才会更新?
|
|
|
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) {
|
|
|
setState(() {
|
|
|
_messages[i].message!.status = event.status;
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
bytedeskEventBus.on<ReceiveMessagePreviewEventBus>().listen((event) {
|
|
|
// BytedeskUtils.printLog('消息预知');
|
|
|
if (this.mounted) {
|
|
|
setState(() {
|
|
|
// TODO: 国际化,支持英文
|
|
|
_title = '对方正在输入';
|
|
|
});
|
|
|
}
|
|
|
// 还原title
|
|
|
Timer(
|
|
|
Duration(seconds: 3),
|
|
|
() {
|
|
|
// BytedeskUtils.printLog('timer');
|
|
|
if (this.mounted) {
|
|
|
setState(() {
|
|
|
_title = widget.title;
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
);
|
|
|
});
|
|
|
// 同 DeleteMessageEventBus 事件
|
|
|
// bytedeskEventBus.on<ReceiveMessageRecallEventBus>().listen((event) {
|
|
|
// BytedeskUtils.printLog('消息撤回');
|
|
|
// });
|
|
|
// 接收到新消息
|
|
|
bytedeskEventBus.on<ReceiveMessageEventBus>().listen((event) {
|
|
|
// BytedeskUtils.printLog('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!);
|
|
|
}
|
|
|
// 界面显示
|
|
|
MessageWidget messageWidget = new MessageWidget(
|
|
|
message: event.message,
|
|
|
customCallback: widget.customCallback,
|
|
|
animationController: new AnimationController(
|
|
|
vsync: this, duration: Duration(milliseconds: 500)));
|
|
|
if (this.mounted) {
|
|
|
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) {
|
|
|
BytedeskUtils.printLog(
|
|
|
'aid ${event.aid}, question ${event.question}, answer ${event.answer}');
|
|
|
// 可以直接将问题和答案插入本地,并显示,但为了服务器保存查询记录,特将请求发送给服务器
|
|
|
// BlocProvider.of<MessageBloc>(context)
|
|
|
// ..add(QueryAnswerEvent(
|
|
|
// tid: _currentThread!.tid,
|
|
|
// aid: event.aid,
|
|
|
// ));
|
|
|
}
|
|
|
});
|
|
|
// 滚动监听, https://learnku.com/articles/30338
|
|
|
_scrollController.addListener(() {
|
|
|
// 隐藏软键盘
|
|
|
FocusScope.of(context).requestFocus(FocusNode());
|
|
|
// 如果滑动到底部
|
|
|
// if (_scrollController.position.pixels ==
|
|
|
// _scrollController.position.maxScrollExtent) {
|
|
|
// BytedeskUtils.printLog('已经到底了');
|
|
|
// }
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 选择图片
|
|
|
Future<void> _pickImage() async {
|
|
|
try {
|
|
|
XFile? pickedFile = await _picker.pickImage(
|
|
|
source: ImageSource.gallery, maxWidth: 800, imageQuality: 95);
|
|
|
//
|
|
|
if (pickedFile != null) {
|
|
|
BytedeskUtils.printLog('pick image path: ${pickedFile.path}');
|
|
|
// TODO: 将图片显示到对话消息中
|
|
|
// TODO: 显示处理中loading
|
|
|
// 压缩
|
|
|
// final dir = await path_provider.getTemporaryDirectory();
|
|
|
// final targetPath = dir.absolute.path +
|
|
|
// "/" +
|
|
|
// BytedeskUtils.currentTimeMillis().toString() +
|
|
|
// ".jpg";
|
|
|
// BytedeskUtils.printLog('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) {
|
|
|
BytedeskUtils.printLog('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) {
|
|
|
BytedeskUtils.printLog('take image path: ${pickedFile.path}');
|
|
|
// TODO: 将图片显示到对话消息中
|
|
|
// TODO: 显示处理中loading
|
|
|
// 压缩
|
|
|
// final dir = await path_provider.getTemporaryDirectory();
|
|
|
// final targetPath = dir.absolute.path +
|
|
|
// "/" +
|
|
|
// BytedeskUtils.currentTimeMillis().toString() +
|
|
|
// ".jpg";
|
|
|
// BytedeskUtils.printLog('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) {
|
|
|
BytedeskUtils.printLog('take image error ${e.toString()}');
|
|
|
Fluttertoast.showToast(msg: "未选取图片");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 上传视频
|
|
|
// FIXME: image_picker有bug,选择视频后缀为.jpg
|
|
|
// 手机可以播放,但chrome无法播放
|
|
|
Future<void> _pickVideo() async {
|
|
|
try {
|
|
|
XFile? pickedFile = await _picker.pickVideo(
|
|
|
source: ImageSource.gallery, maxDuration: const Duration(seconds: 10));
|
|
|
|
|
|
if (pickedFile != null) {
|
|
|
BytedeskUtils.printLog('pick video path: ${pickedFile.path}');
|
|
|
//
|
|
|
BlocProvider.of<MessageBloc>(context)
|
|
|
..add(UploadVideoEvent(filePath: pickedFile.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)
|
|
|
// // );
|
|
|
// // // debugBytedeskUtils.printLog(info.toJson().toString());
|
|
|
// // String? afterPath = info.toJson()['path'];
|
|
|
// // // BytedeskUtils.printLog('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) {
|
|
|
BytedeskUtils.printLog('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) {
|
|
|
BytedeskUtils.printLog('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)
|
|
|
// );
|
|
|
// // debugBytedeskUtils.printLog(info.toJson().toString());
|
|
|
// String? afterPath = info.toJson()['path'];
|
|
|
// // BytedeskUtils.printLog('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) {
|
|
|
BytedeskUtils.printLog('take video error ${e.toString()}');
|
|
|
Fluttertoast.showToast(msg: "未录制视频");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 加载更多聊天记录
|
|
|
// Future<void> _loadMoreMessages() async {
|
|
|
// BytedeskUtils.printLog('load more');
|
|
|
// // TODO: 从服务器加载
|
|
|
// _getMessages(_page, _size);
|
|
|
// }
|
|
|
|
|
|
// 分页加载本地历史聊天记录
|
|
|
// TODO: 从服务器加载聊天记录
|
|
|
Future<Null> _getMessages(int page, int size) async {
|
|
|
//
|
|
|
List<Message> messageList = await _messageProvider.getTopicMessages(
|
|
|
_currentThread!.topic, _currentUid, page, size);
|
|
|
messageList.forEach((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);
|
|
|
_messages.add(messageWidget);
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
//
|
|
|
_page += 1;
|
|
|
}
|
|
|
|
|
|
Future<Null> _appendMessage(Message message) async {
|
|
|
// BytedeskUtils.printLog('append:' + message!.mid);
|
|
|
bool contains = false;
|
|
|
for (var i = 0; i < _messages.length; i++) {
|
|
|
Message? element = _messages[i].message;
|
|
|
if (element?.mid == message.mid) {
|
|
|
contains = true;
|
|
|
}
|
|
|
}
|
|
|
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) {
|
|
|
// BytedeskUtils.printLog("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() {
|
|
|
// BytedeskUtils.printLog('chat_kf_page dispose');
|
|
|
SpUtil.putBool(BytedeskConstants.isCurrentChatKfPage, false);
|
|
|
WidgetsBinding.instance!.removeObserver(this);
|
|
|
_debounce?.cancel();
|
|
|
_loadHistoryTimer?.cancel();
|
|
|
super.dispose();
|
|
|
}
|
|
|
}
|