diff --git a/lib/models/chat/call.dart b/lib/models/chat/call.dart new file mode 100644 index 0000000..eacdd03 --- /dev/null +++ b/lib/models/chat/call.dart @@ -0,0 +1,117 @@ +class IncomingCallData { + String? callerID; + String? receiverID; + String? msgID; + String? notfID; + String? notificationForeground; + String? count; + String? message; + String? appointmentNo; + String? title; + String? projectID; + String? notificationType; + String? background; + String? doctorname; + String? clinicname; + String? speciality; + String? appointmentdate; + String? appointmenttime; + String? type; + String? sessionId; + String? identity; + String? name; + String? videoUrl; + String? picture; + String? token; + String? isCall; + String? sound; + String? server; + String? isWebRTC; + + IncomingCallData( + {this.msgID, + this.notfID, + this.notificationForeground, + this.count, + this.message, + this.appointmentNo, + this.title, + this.projectID, + this.notificationType, + this.background, + this.doctorname, + this.clinicname, + this.speciality, + this.appointmentdate, + this.appointmenttime, + this.type, + this.sessionId, + this.identity, + this.name, + this.videoUrl, + this.picture, + this.isCall, + this.sound}); + + IncomingCallData.fromJson(Map json) { + callerID = json['callerID']; + receiverID = json['PatientID']; + msgID = json['msgID']; + notfID = json['notfID']; + notificationForeground = json['notification_foreground']; + count = json['count']; + message = json['message']; + appointmentNo = json['AppointmentNo']; + title = json['title']; + projectID = json['ProjectID']; + notificationType = json['NotificationType']; + background = json['background']; + doctorname = json['doctorname']; + clinicname = json['clinicname']; + speciality = json['speciality']; + appointmentdate = json['appointmentdate']; + appointmenttime = json['appointmenttime']; + type = json['type']; + sessionId = json['session_id']; + token = json['token']; + identity = json['identity']; + name = json['name']; + videoUrl = json['videoUrl']; + picture = json['picture']; + isCall = json['is_call']; + sound = json['sound']; + server = json['server']; + isWebRTC = json['is_webrtc'] ?? "true"; + } + + Map toJson() { + Map data = Map(); + data['msgID'] = this.msgID; + data['notfID'] = this.notfID; + data['notification_foreground'] = this.notificationForeground; + data['count'] = this.count; + data['message'] = this.message; + data['AppointmentNo'] = this.appointmentNo; + data['title'] = this.title; + data['ProjectID'] = this.projectID; + data['NotificationType'] = this.notificationType; + data['background'] = this.background; + data['doctorname'] = this.doctorname; + data['clinicname'] = this.clinicname; + data['speciality'] = this.speciality; + data['appointmentdate'] = this.appointmentdate; + data['appointmenttime'] = this.appointmenttime; + data['type'] = this.type; + data['session_id'] = this.sessionId; + data['token'] = this.token; + data['identity'] = this.identity; + data['name'] = this.name; + data['videoUrl'] = this.videoUrl; + data['picture'] = this.picture; + data['is_call'] = this.isCall; + data['sound'] = this.sound; + data['server'] = this.server; + data['is_webrtc'] = this.isWebRTC; + return data; + } +} diff --git a/lib/ui/chat/call/chat_call_screen.dart b/lib/ui/chat/call/chat_call_screen.dart new file mode 100644 index 0000000..0bec1f0 --- /dev/null +++ b/lib/ui/chat/call/chat_call_screen.dart @@ -0,0 +1,379 @@ +import 'dart:ui'; + +import 'package:camera/camera.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/models/chat/call.dart'; + +class IncomingCall extends StatefulWidget { + IncomingCallData incomingCallData; + bool? isVideoCall; + + IncomingCall({Key? key, required this.incomingCallData, this.isVideoCall}) : super(key: key); + + @override + _IncomingCallState createState() => _IncomingCallState(); +} + +class _IncomingCallState extends State with SingleTickerProviderStateMixin { + AnimationController? _animationController; + CameraController? _controller; + Future? _initializeControllerFuture; + bool isCameraReady = false; + + @override + void initState() { + _animationController = AnimationController( + vsync: this, + duration: const Duration( + milliseconds: 500, + ), + ); + //_runAnimation(); + // connectSignaling(); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _runAnimation(), + ); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: FutureBuilder( + future: _initializeControllerFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Stack( + alignment: FractionalOffset.center, + children: [ + if (widget.isVideoCall!) + Positioned.fill( + child: AspectRatio( + aspectRatio: _controller!.value.aspectRatio, + child: CameraPreview( + _controller!, + ), + ), + ), + Positioned.fill( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: Container( + decoration: BoxDecoration( + color: MyColors.grey57Color.withOpacity( + 0.7, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Container( + margin: const EdgeInsets.all(21.0), + child: Row( + children: [ + Image.asset( + "assets/images/logos/main_mohemm_logo.png", + height: 70, + width: 70, + ), + Container( + margin: const EdgeInsets.only( + left: 10.0, + right: 10.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: const [ + Text( + "Aamir Saleem Ahmad", + style: TextStyle( + fontSize: 21, + fontWeight: FontWeight.bold, + color: MyColors.white, + letterSpacing: -1.26, + height: 23 / 12, + ), + ), + Text( + "Calling...", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color( + 0xffC6C6C6, + ), + letterSpacing: -0.48, + height: 23 / 24, + ), + ), + SizedBox( + height: 2, + ), + ], + ), + ), + ], + ), + ), + // Container( + // margin: const EdgeInsets.all(21.0), + // width: MediaQuery.of(context).size.width, + // decoration: cardRadius(15.0, color: MyColors.black, elevation: null), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisSize: MainAxisSize.min, + // children: [ + // Container( + // padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 6.0), + // child: Text( + // "TranslationBase.of(context).appoInfo", + // style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.white, letterSpacing: -0.64, height: 23 / 12), + // ), + // ), + // Container( + // padding: const EdgeInsets.only(left: 16.0, right: 16.0), + // child: Text( + // "widget.incomingCallData.appointmentdate + widget.incomingCallData.appointmenttime", + // style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600), + // ), + // ), + // Container( + // padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 21.0), + // child: Text( + // "widget.incomingCallData.clinicname", + // style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600), + // ), + // ), + // ], + // ), + // ), + const Spacer(), + Container( + margin: const EdgeInsets.only( + bottom: 70.0, + left: 49, + right: 49, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RotationTransition( + turns: Tween( + begin: 0.0, + end: -.1, + ) + .chain( + CurveTween( + curve: Curves.elasticIn, + ), + ) + .animate( + _animationController!, + ), + child: RawMaterialButton( + onPressed: () { + _submit(); + }, + elevation: 2.0, + fillColor: MyColors.green2DColor, + padding: const EdgeInsets.all( + 15.0, + ), + shape: const CircleBorder(), + child: const Icon( + Icons.call, + color: MyColors.white, + size: 35.0, + ), + ), + ), + RawMaterialButton( + onPressed: () { + backToHome(); + }, + elevation: 2.0, + fillColor: MyColors.redA3Color, + padding: const EdgeInsets.all( + 15.0, + ), + shape: const CircleBorder(), + child: const Icon( + Icons.call_end, + color: MyColors.white, + size: 35.0, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ], + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + ); + } + + void _runAnimation() async { + List cameras = await availableCameras(); + CameraDescription firstCamera = cameras[1]; + _controller = CameraController( + firstCamera, + ResolutionPreset.medium, + ); + _initializeControllerFuture = _controller!.initialize(); + setState(() {}); + // setAudioFile(); + for (int i = 0; i < 100; i++) { + await _animationController!.forward(); + await _animationController!.reverse(); + } + } + + Future _submit() async { + try { + // backToHome(); + // final roomModel = RoomModel(name: widget.incomingCallData.name, token: widget.incomingCallData.sessionId, identity: widget.incomingCallData.identity); + await _controller?.dispose(); + // changeCallStatusAPI(4); + + // if (_session != null && _signaling != null) { + // await Navigator.of(context).pushReplacement( + // MaterialPageRoute( + // // fullscreenDialog: true, + // builder: (BuildContext context) { + // // if (widget.incomingCallData.isWebRTC == "true") { + // return StartVideoCall(signaling: _signaling, session: _session); + // + // // else { + // // return OpenTokConnectCallPage(apiKey: OPENTOK_API_KEY, sessionId: widget.incomingCallData.sessionId, token: widget.incomingCallData.token); + // // } + // + // // return VideoCallWebPage(receiverId: widget.incomingCallData.receiverID, callerId: widget.incomingCallData.callerID); // Web WebRTC VideoCall + // + // // return CallHomePage(receiverId: widget.incomingCallData.receiverID, callerId: widget.incomingCallData.callerID); // App WebRTC VideoCall + // }, + // ), + // ); + // } else { + // // Invalid Params/Data + // Utils.showToast("Failed to establish connection with server"); + // } + } catch (err) { + print(err); + // await PlatformExceptionAlertDialog( + // exception: err, + // ).show(context); + + Utils.showToast(err.toString()); + } + } + + // void changeCallStatusAPI(int sessionStatus) { + // LiveCareService service = new LiveCareService(); + // service.endCallAPI(widget.incomingCallData.sessionId, sessionStatus, context).then((res) {}).catchError((err) { + // print(err); + // }); + // } + + void backToHome() async { + // final connected = await signaling.declineCall(widget.incomingCallData.callerID, widget.incomingCallData.receiverID); + // LandingPage.isOpenCallPage = false; + // _signaling + // player.stop(); + // changeCallStatusAPI(3); + // _signaling.bye(_session, callRejected: true); + // _signaling.callDisconnected(_session, callRejected: true); + Navigator.of(context).pop(); + } + + // + // void disposeAudioResources() async { + // await player.dispose(); + // } + // + // void setAudioFile() async { + // player.stop(); + // await player.setVolume(1.0); // full volume + // try { + // await player.setAsset('assets/sounds/ring_60Sec.mp3').then((value) { + // player.setLoopMode(LoopMode.one); // loop ring sound + // player.play(); + // }).catchError((err) { + // print("Error: $err"); + // }); + // } catch (e) { + // print("Error: $e"); + // } + // } + // + // void connectSignaling({@required bool iAmCaller = false}) async { + // print("----------------- + Signaling Connection Started ---------------------------"); + // var caller = widget.incomingCallData.callerID; + // var receiver = widget.incomingCallData.receiverID; + // var host = widget.incomingCallData.server; + // + // var selfRole = iAmCaller ? "Caller" : "Receiver"; + // var selfId = iAmCaller ? caller : receiver; + // var selfUser = SocketUser(id: selfId, name: "$selfRole-$selfId", userAgent: DeviceInfo.userAgent, moreInfo: {}); + // + // var remoteRole = !iAmCaller ? "Caller" : "Receiver"; + // var remoteId = !iAmCaller ? caller : receiver; + // var remoteUser = SocketUser(id: remoteId, name: "$remoteRole-$remoteId", userAgent: DeviceInfo.userAgent, moreInfo: {}); + // + // var sessionId = "$caller-$receiver"; + // _session = SessionOneToOne(id: sessionId, local_user: selfUser, remote_user: remoteUser); + // + // _signaling = Signaling(host, session: _session); + // await _signaling.connect(); + // + // if (_signaling.state == SignalingState.Open) { + // return; + // } + // } + + BoxDecoration cardRadius(double radius, {required Color color, double? elevation}) { + return BoxDecoration( + shape: BoxShape.rectangle, + color: color ?? Colors.white, + borderRadius: BorderRadius.all( + Radius.circular(radius), + ), + boxShadow: [ + BoxShadow( + color: const Color( + 0xff000000, + ).withOpacity( + .05, + ), + //spreadRadius: 5, + blurRadius: elevation ?? 27, + offset: const Offset( + -2, + 3, + ), + ), + ], + ); + } +} diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index 782e4e2..6ddd19e 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -9,6 +9,8 @@ import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/models/chat/call.dart'; +import 'package:mohem_flutter_app/ui/chat/call/chat_call_screen.dart'; import 'package:mohem_flutter_app/ui/chat/chat_bubble.dart'; import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; @@ -16,15 +18,18 @@ import 'package:provider/provider.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:swipe_to/swipe_to.dart'; -class ChatDetailScreen extends StatelessWidget { +class ChatDetailScreen extends StatefulWidget { // ignore: prefer_const_constructors_in_immutables ChatDetailScreen({Key? key}) : super(key: key); - dynamic userDetails; + @override + State createState() => _ChatDetailScreenState(); +} +class _ChatDetailScreenState extends State { + dynamic userDetails; late ChatProviderModel data; - - final RefreshController _refreshController = RefreshController(initialRefresh: false); + final RefreshController _rc = RefreshController(initialRefresh: false); void getMoreChat() async { if (userDetails != null) { @@ -32,19 +37,54 @@ class ChatDetailScreen extends StatelessWidget { data.getSingleUserChatHistory(senderUID: AppState().chatDetails!.response!.id.toString(), receiverUID: userDetails["targetUser"].id, loadMore: true, isNewChat: false); } await Future.delayed(const Duration(milliseconds: 1000)); - _refreshController.loadComplete(); + _rc.loadComplete(); } @override - Widget build(BuildContext context) { - userDetails = ModalRoute.of(context)!.settings.arguments; + void initState() { + super.initState(); data = Provider.of(context, listen: false); if (userDetails != null) - data.getSingleUserChatHistory(senderUID: AppState().chatDetails!.response!.id.toString(), receiverUID: userDetails["targetUser"].id, loadMore: false, isNewChat: userDetails["isNewChat"]); - data.scrollController.addListener(data.scrollListener); + data.getSingleUserChatHistory( + senderUID: AppState().chatDetails!.response!.id.toString(), + receiverUID: userDetails["targetUser"].id, + loadMore: false, + isNewChat: userDetails["isNewChat"], + ); + //data.scrollController.addListener(data.scrollListener); + } + + @override + Widget build(BuildContext context) { + userDetails = ModalRoute.of(context)!.settings.arguments; return Scaffold( backgroundColor: const Color(0xFFF8F8F8), - appBar: AppBarWidget(context, title: userDetails["targetUser"].userName.toString().replaceAll(".", " ").capitalizeFirstofEach, showHomeButton: false, image: userDetails["targetUser"].image), + appBar: AppBarWidget(context, + title: userDetails["targetUser"].userName.toString().replaceAll(".", " ").capitalizeFirstofEach, + showHomeButton: false, + image: userDetails["targetUser"].image, + actions: [ + IconButton( + onPressed: () { + makeCall("AUDIO"); + }, + icon: SvgPicture.asset( + "assets/images/call.svg", + width: 25, + height: 25, + ), + ), + IconButton( + onPressed: () { + makeCall("VIDEO"); + }, + icon: SvgPicture.asset( + "assets/images/call.svg", + width: 25, + height: 25, + ), + ), + ]), body: Consumer( builder: (BuildContext context, ChatProviderModel m, Widget? child) { return (m.isLoading @@ -62,7 +102,7 @@ class ChatDetailScreen extends StatelessWidget { header: const MaterialClassicHeader( color: MyColors.gradiantEndColor, ), - controller: _refreshController, + controller: _rc, reverse: true, child: ListView.builder( controller: m.scrollController, @@ -136,11 +176,11 @@ class ChatDetailScreen extends StatelessWidget { margin: EdgeInsets.zero, elevation: 0, child: Padding( - padding: const EdgeInsets.only(left: 20.0, right: 20, top: 20, bottom: 0), + padding: const EdgeInsets.only(left: 20, right: 20, top: 20, bottom: 0), child: Card( margin: EdgeInsets.zero, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(0.0), + borderRadius: BorderRadius.circular(0), ), elevation: 0, child: Container( @@ -267,4 +307,54 @@ class ChatDetailScreen extends StatelessWidget { ), ); } + + void makeCall(String callType) async { + // final server = await SelectionDialog( + // context, + // title: "Select Server", + // items: ["https://livecareturn.hmg.com:8086", "https://104.197.179.1:8086"] + // ).show(); + + Map json = { + "callerID": "9920", + "PatientID": "1231755", + "msgID": "123", + "notfID": "123", + "notification_foreground": "true", + "count": "1", + "message": "Doctor is calling ", + "AppointmentNo": "123", + "title": "Rayyan Hospital", + "ProjectID": "123", + "NotificationType": "10", + "background": "1", + "doctorname": "Dr Sulaiman Al Habib", + "clinicname": "ENT Clinic", + "speciality": "Speciality", + "appointmentdate": "Sun, 15th Dec, 2019", + "appointmenttime": "09:00", + "type": "video", + "session_id": + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImN0eSI6InR3aWxpby1mcGE7dj0xIn0.eyJqdGkiOiJTS2I2NjYyOWMzN2ZhOTM3YjFjNDI2Zjg1MTgyNWFmN2M0LTE1OTg3NzQ1MDYiLCJpc3MiOiJTS2I2NjYyOWMzN2ZhOTM3YjFjNDI2Zjg1MTgyNWFmN2M0Iiwic3ViIjoiQUNhYWQ1YTNmOGM2NGZhNjczNTY3NTYxNTc0N2YyNmMyYiIsImV4cCI6MTU5ODc3ODEwNiwiZ3JhbnRzIjp7ImlkZW50aXR5IjoiSGFyb29uMSIsInZpZGVvIjp7InJvb20iOiJTbWFsbERhaWx5U3RhbmR1cCJ9fX0.7XUS5uMQQJfkrBZu9EjQ6STL6R7iXkso6BtO1HmrQKk", + "identity": "Haroon1", + "name": "SmallDailyStandup", + "videoUrl": "video", + "picture": "video", + "is_call": "true", + "is_webrtc": "true", + // "server": "https://192.168.8.163:8086", + "server": "https://livecareturn.hmg.com:8086", + }; + + IncomingCallData incomingCallData = IncomingCallData.fromJson(json); + await Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => IncomingCall( + incomingCallData: incomingCallData, + isVideoCall: callType == "VIDEO" ? true : false, + ), + ), + ); + } } diff --git a/lib/widgets/app_bar_widget.dart b/lib/widgets/app_bar_widget.dart index 745dedf..d3e7c97 100644 --- a/lib/widgets/app_bar_widget.dart +++ b/lib/widgets/app_bar_widget.dart @@ -7,11 +7,7 @@ import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; AppBar AppBarWidget(BuildContext context, - {required String title, - bool showHomeButton = true, - bool showNotificationButton = false, - bool showMemberButton = false, - String? image}) { + {required String title, bool showHomeButton = true, bool showNotificationButton = false, bool showMemberButton = false, String? image, bool, List? actions}) { return AppBar( leadingWidth: 0, // leading: GestureDetector( @@ -24,17 +20,16 @@ AppBar AppBarWidget(BuildContext context, children: [ GestureDetector( behavior: HitTestBehavior.opaque, - onTap: - Feedback.wrapForTap(() => Navigator.maybePop(context), context), - child: - const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), + onTap: Feedback.wrapForTap(() => Navigator.maybePop(context), context), + child: const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), ), 4.width, - if (image != null) SvgPicture.asset( - image, - height: 40, - width: 40, - ), + if (image != null) + SvgPicture.asset( + image, + height: 40, + width: 40, + ), if (image != null) 14.width, title.toText24(color: MyColors.darkTextColor, isBold: true).expanded, ], @@ -46,8 +41,7 @@ AppBar AppBarWidget(BuildContext context, if (showHomeButton) IconButton( onPressed: () { - Navigator.popUntil( - context, ModalRoute.withName(AppRoutes.dashboard)); + Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)); }, icon: const Icon(Icons.home, color: MyColors.darkIconColor), ), @@ -65,6 +59,9 @@ AppBar AppBarWidget(BuildContext context, }, icon: const Icon(Icons.people, color: MyColors.textMixColor), ), + + + ...actions??[] ], ); } diff --git a/pubspec.yaml b/pubspec.yaml index 8947c12..5a7c1c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,6 +91,8 @@ dependencies: signalr_netcore: ^1.3.3 logging: ^1.0.1 swipe_to: ^1.0.2 + flutter_webrtc: ^0.9.16 + camera: ^0.10.0+4