Created
July 2, 2024 13:04
-
-
Save DhruvamUnikon/0d7b8735751acdce42cdd564c966c8f6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'dart:convert'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter_screenutil/flutter_screenutil.dart'; | |
import 'package:provider/provider.dart'; | |
import 'package:unikon/features/post/data/model/reaction/short_user.dart'; | |
import 'package:unikon/features/post/domain/entity/reaction/short_user_vm.dart'; | |
import 'package:unikon/utils/api/constants.dart'; | |
import 'package:unikon/utils/bottom_sheet/base_bottom_sheet.dart'; | |
import 'package:unikon/utils/dimensions/gaps.dart'; | |
import 'package:unikon/utils/env/environment.dart'; | |
import 'package:unikon/utils/extensions/text/hardcoded.dart'; | |
import 'package:unikon/utils/images/constants.dart'; | |
import 'package:unikon/utils/logout/login_provider.dart'; | |
import 'package:unikon/utils/null_checker/null_checker.dart'; | |
import 'package:unikon/utils/router/router_config.dart'; | |
import 'package:unikon/utils/user/user_details.dart'; | |
import 'package:unikon/widget_library/avatars/profile_avatar.dart'; | |
import 'package:unikon/widget_library/buttons/inverse_primary_button.dart'; | |
import 'package:unikon/widget_library/buttons/primary_button.dart'; | |
import 'package:unikon/widget_library/images/base_image.dart'; | |
import 'package:unikon/widget_library/video_call/advanced_video_call_holder.dart'; | |
import 'package:zego_uikit_prebuilt_call/zego_uikit_prebuilt_call.dart'; | |
import 'package:zego_uikit_signaling_plugin/zego_uikit_signaling_plugin.dart'; | |
class CallInvitationController { | |
CallInvitationController._internal(); | |
static final CallInvitationController instance = | |
CallInvitationController._internal(); | |
final onCallEndStreamCtrl = StreamController<String>.broadcast(); | |
Future<bool> onCallHangUp() async { | |
final canHangUp = await CustomBottomSheet.show( | |
navigatorKey.currentContext!, | |
const CallCloseBottomSheetWidget(), | |
); | |
return (canHangUp != null && canHangUp is bool && canHangUp); | |
} | |
} | |
class ZegoCallInvitation { | |
/// Step 1 | |
/// Setup navigator key | |
static void setUpNavigatorKey(GlobalKey<NavigatorState> navigatorKey) { | |
ZegoUIKitPrebuiltCallInvitationService().setNavigatorKey(navigatorKey); | |
} | |
/// Step 2 | |
/// Initialise Calling UI | |
static void initialiseCalUI(Function onInvitationSetupDone) { | |
/// call the useSystemCallingUI | |
ZegoUIKit().initLog().then((value) { | |
ZegoUIKitPrebuiltCallInvitationService().useSystemCallingUI( | |
[ZegoUIKitSignalingPlugin()], | |
); | |
onInvitationSetupDone(); | |
}); | |
} | |
/// on App's user login | |
static Future<void> onUserLogin(String userId, String userName) async { | |
/// 1.2.1. initialized ZegoUIKitPrebuiltCallInvitationService | |
/// when app's user is logged in or re-logged in | |
/// We recommend calling this method as soon as the user logs in to your app. | |
await ZegoUIKitPrebuiltCallInvitationService().init( | |
appID: AppEnvironment.instance.zegoAppId /*input your AppID*/, | |
appSign: AppEnvironment.instance.zegoAppSign /*input your AppSign*/, | |
userID: 'user_$userId', | |
userName: userName, | |
plugins: [ZegoUIKitSignalingPlugin()], | |
ringtoneConfig: ZegoCallRingtoneConfig( | |
incomingCallPath: 'assets/sounds/phone_ringing.mp3', | |
outgoingCallPath: 'assets/sounds/phone_ringing.mp3', | |
), | |
notificationConfig: ZegoCallInvitationNotificationConfig( | |
androidNotificationConfig: ZegoCallAndroidNotificationConfig( | |
showFullScreen: true, | |
), | |
), | |
events: ZegoUIKitPrebuiltCallEvents( | |
onHangUpConfirmation: (event, defaultAction) async { | |
return CallInvitationController.instance.onCallHangUp(); | |
}, | |
onCallEnd: (event, defaultAction) { | |
defaultAction.call(); | |
CallInvitationController.instance.onCallEndStreamCtrl.add('call-end'); | |
}, | |
), | |
invitationEvents: ZegoUIKitPrebuiltCallInvitationEvents( | |
onOutgoingCallTimeout: (callId, callee, isVideoCall) { | |
CallInvitationController.instance.onCallEndStreamCtrl.add('call-end'); | |
}, | |
onError: (error) {}, | |
), | |
requireConfig: (ZegoCallInvitationData data) { | |
final isVideoCall = data.type == ZegoCallType.videoCall; | |
final Map<String, dynamic> customData = jsonDecode(data.customData); | |
final callee = ShortUser.fromJson(customData[ApiConstants.calleeKey]); | |
final caller = ShortUser.fromJson(customData[ApiConstants.callerKey]); | |
final endTime = | |
DateTime.parse(customData[ApiConstants.endTimeCamelCaseKey]); | |
final duration = customData[ApiConstants.durationCamelCaseKey]; | |
final loggedInUserId = 'user_$userId'; | |
final calleeId = 'user_${callee.id}'; | |
final callerId = 'user_${caller.id}'; | |
final isGroupCall = data.invitees.length > 1; | |
late ZegoUIKitPrebuiltCallConfig config; | |
if (isVideoCall) { | |
if (isGroupCall) { | |
config = ZegoUIKitPrebuiltCallConfig.groupVideoCall(); | |
} else { | |
config = ZegoUIKitPrebuiltCallConfig.oneOnOneVideoCall(); | |
} | |
} else { | |
if (isGroupCall) { | |
config = ZegoUIKitPrebuiltCallConfig.groupVoiceCall(); | |
} else { | |
config = ZegoUIKitPrebuiltCallConfig.oneOnOneVoiceCall(); | |
} | |
} | |
config.layout = ZegoLayout.gallery(); | |
config.bottomMenuBar = ZegoCallBottomMenuBarConfig( | |
isVisible: data.type == ZegoCallType.videoCall, | |
); | |
avatarBuilder(context, size, user, extraInfo) { | |
if (user?.id == callerId) { | |
return ProfileAvatarWidget( | |
radius: size.height, | |
firstName: caller.firstName, | |
lastName: caller.lastName, | |
url: caller.profilePictureUrl, | |
); | |
} else { | |
return ProfileAvatarWidget( | |
radius: size.height, | |
firstName: callee.firstName, | |
lastName: callee.lastName, | |
url: callee.profilePictureUrl, | |
); | |
} | |
} | |
config.avatarBuilder = avatarBuilder; | |
// When joining using audio call, switch to audio output | |
// device rather than speaker and turn off camera | |
config.turnOnMicrophoneWhenJoining = true; | |
config.turnOnCameraWhenJoining = isVideoCall; | |
config.useSpeakerWhenJoining = isVideoCall; | |
config.duration = ZegoCallDurationConfig( | |
isVisible: false, | |
); | |
config.audioVideoView = ZegoCallAudioVideoViewConfig( | |
showCameraStateOnView: false, | |
showMicrophoneStateOnView: false, | |
showUserNameOnView: false, | |
useVideoViewAspectFill: true, | |
isVideoMirror: false, | |
containerBuilder: ( | |
context, | |
allUsers, | |
audioVideoUsers, | |
audioVideoViewCreator, | |
) { | |
if (data.type == ZegoCallType.voiceCall) { | |
return AudioCallView( | |
endTime: endTime, | |
duration: duration, | |
user: callerId == loggedInUserId ? callee : caller, | |
); | |
} | |
// find caller and callee if it is a video call | |
final callerZegoUser = allUsers.firstWhere( | |
(element) => element.id == callerId, | |
orElse: () => ZegoUIKitUser( | |
id: callerId, | |
name: caller.firstName, | |
), | |
); | |
final calleeZegoUser = allUsers.firstWhere( | |
(element) => element.id == calleeId, | |
orElse: () => ZegoUIKitUser( | |
id: calleeId, | |
name: callee.firstName, | |
), | |
); | |
return VideoCallView( | |
endTime: endTime, | |
duration: duration, | |
caller: loggedInUserId == callerId ? caller : callee, | |
callee: loggedInUserId == callerId ? callee : caller, | |
callerView: ZegoAudioVideoView( | |
avatarConfig: ZegoAvatarConfig( | |
builder: (context, size, user, extraInfo) => | |
avatarBuilder(context, size, user, extraInfo), | |
), | |
backgroundBuilder: (context, size, user, extraInfo) { | |
return Container( | |
color: Colors.black, | |
); | |
}, | |
borderRadius: 0, | |
user: loggedInUserId == callerId | |
? callerZegoUser | |
: calleeZegoUser, | |
), | |
calleeView: ZegoAudioVideoView( | |
backgroundBuilder: (context, size, user, extraInfo) { | |
return Container( | |
color: Colors.black, | |
); | |
}, | |
avatarConfig: ZegoAvatarConfig( | |
builder: (context, size, user, extraInfo) => avatarBuilder( | |
context, | |
size, | |
user, | |
extraInfo, | |
), | |
), | |
borderRadius: 0, | |
user: loggedInUserId == callerId | |
? calleeZegoUser | |
: callerZegoUser, | |
), | |
); | |
}, | |
); | |
return config; | |
}, | |
config: ZegoCallInvitationConfig( | |
permissions: const [], | |
), | |
); | |
} | |
/// on App's user logout | |
static Future<void> onUserLogout() async { | |
/// 1.2.2. de-initialization ZegoUIKitPrebuiltCallInvitationService | |
/// when app's user is logged out | |
await ZegoUIKitPrebuiltCallInvitationService().uninit(); | |
} | |
static Future<void> sendCallInvitation( | |
// Callee user ID | |
ShortUserVM callee, | |
// call id that you can use to identify the call | |
String callId, | |
// call end time | |
DateTime endTime, | |
// call duration | |
num duration, | |
// call type: video or voice | |
ZegoCallType callType, | |
) async { | |
final loggedInUser = | |
navigatorKey.currentContext!.read<LoggedInStateProvider>().user; | |
if (loggedInUser == null) { | |
return; | |
} | |
final caller = loggedInUser.toShortUserModel(); | |
/// 1.3.1. send call invitation | |
await ZegoUIKitPrebuiltCallInvitationService().send( | |
invitees: [ | |
ZegoCallUser( | |
'user_${callee.id}', | |
callee.firstName, | |
), | |
], | |
isVideoCall: callType == ZegoCallType.videoCall, | |
resourceID: 'call_invitation', | |
callID: callId, | |
customData: jsonEncode({ | |
ApiConstants.calleeKey: callee.toMap(), | |
ApiConstants.callerKey: caller.toMap(), | |
ApiConstants.durationCamelCaseKey: duration, | |
ApiConstants.endTimeCamelCaseKey: endTime.toIso8601String(), | |
}), | |
); | |
} | |
} | |
class VideoCallView extends StatelessWidget { | |
final ShortUserVM caller; | |
final ShortUserVM callee; | |
final ZegoAudioVideoView callerView; | |
final ZegoAudioVideoView calleeView; | |
final DateTime endTime; | |
final num duration; | |
const VideoCallView({ | |
super.key, | |
required this.caller, | |
required this.callee, | |
required this.callerView, | |
required this.calleeView, | |
required this.endTime, | |
required this.duration, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
color: Colors.transparent, | |
child: Stack( | |
children: [ | |
Column( | |
children: [ | |
Expanded( | |
child: Stack( | |
children: [ | |
calleeView, | |
Positioned( | |
bottom: 0, | |
left: 0, | |
child: UserDetailsWidget(user: callee), | |
), | |
], | |
), | |
), | |
const UnikonBrandingStrip(), | |
Expanded( | |
child: Stack( | |
children: [ | |
callerView, | |
Positioned( | |
bottom: 0, | |
left: 0, | |
child: UserDetailsWidget(user: caller), | |
), | |
], | |
), | |
), | |
], | |
), | |
Container( | |
margin: EdgeInsets.only( | |
top: 20.sp, | |
left: 16.sp, | |
right: 0, | |
), | |
padding: const EdgeInsets.symmetric( | |
horizontal: 8, | |
vertical: 2, | |
), | |
decoration: ShapeDecoration( | |
color: const Color(0xB2232323), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(16.sp), | |
), | |
), | |
child: CallTimerWidget( | |
duration: duration, | |
endTime: endTime, | |
hourColor: Colors.white, | |
minuteColor: Colors.red, | |
secondColor: Colors.red, | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class AudioCallView extends StatelessWidget { | |
final ShortUserVM user; | |
final DateTime endTime; | |
final num duration; | |
const AudioCallView( | |
{super.key, | |
required this.user, | |
required this.endTime, | |
required this.duration}); | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
color: Theme.of(context).scaffoldBackgroundColor, | |
child: Stack( | |
children: [ | |
SingleChildScrollView( | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
SizedBox( | |
height: 40.sp, | |
), | |
SizedBox( | |
height: 20.sp, | |
width: 102.sp, | |
child: const BaseImageWidget( | |
imageUri: ImageConstants.unikonCallLogo, | |
), | |
), | |
g20Box, | |
g20Box, | |
Center( | |
child: ProfileAvatarWidget( | |
radius: 90.sp, | |
firstName: user.firstName, | |
lastName: user.lastName, | |
url: user.profilePictureUrl, | |
), | |
), | |
SizedBox( | |
height: 50.sp, | |
), | |
Text( | |
'${user.firstName} ${user.lastName}', | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 28.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w500, | |
), | |
), | |
Text( | |
UserDetails.buildUserCompanyDetails( | |
position: user.currentDesignation, | |
companyName: user.currentOrganisation), | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 12.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w400, | |
), | |
), | |
if (user.city != null) g4Box, | |
if (user.city != null) | |
Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
BaseImageWidget( | |
imageUri: ImageConstants.callLocationIcon, | |
size: Size(12.sp, 12.sp), | |
), | |
g4Box, | |
Text( | |
NullChecker.buildCityAndCountry( | |
user.city?.name, | |
user.country?.name, | |
), | |
style: TextStyle( | |
color: const Color(0xFFCCCCCC), | |
fontSize: 12.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w300, | |
), | |
), | |
], | |
), | |
const SizedBox( | |
height: 36, | |
), | |
Container( | |
padding: const EdgeInsets.symmetric( | |
horizontal: 8, | |
vertical: 6, | |
), | |
decoration: ShapeDecoration( | |
color: Colors.white.withOpacity(0.20000000298023224), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(10.sp), | |
), | |
), | |
child: CallTimerWidget( | |
duration: duration, | |
endTime: endTime, | |
), | |
), | |
], | |
), | |
), | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: SizedBox( | |
width: double.infinity, | |
child: BaseImageWidget( | |
size: Size(double.infinity, 120.sp), | |
imageUri: ImageConstants.audioCallBottomLayout, | |
color: const Color(0xff242840), | |
), | |
), | |
), | |
Positioned( | |
bottom: 80.sp, | |
left: 0, | |
right: 0, | |
child: CircleAvatar( | |
radius: 30.sp, | |
backgroundColor: Colors.red, | |
child: IconButton( | |
onPressed: () async { | |
ZegoUIKitPrebuiltCallInvitationService().controller.hangUp( | |
context, | |
showConfirmation: true, | |
); | |
}, | |
icon: Icon( | |
Icons.call_end, | |
size: 30.sp, | |
color: Colors.white, | |
), | |
), | |
), | |
), | |
Positioned( | |
bottom: 20.sp, | |
left: 0, | |
right: 0, | |
child: Text( | |
'Your call is secure'.hardcoded, | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 14.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w400, | |
), | |
textAlign: TextAlign.center, | |
), | |
), | |
Positioned( | |
bottom: 32.sp, | |
left: 24.sp, | |
child: Container( | |
width: 40.sp, | |
height: 40.sp, | |
decoration: ShapeDecoration( | |
color: Colors.white.withOpacity(0.20000000298023224), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(12), | |
), | |
), | |
child: ZegoSwitchAudioOutputButton( | |
defaultUseSpeaker: false, | |
speakerIcon: ButtonIcon( | |
icon: Icon( | |
Icons.volume_up, | |
color: Colors.white, | |
size: 24.sp, | |
), | |
backgroundColor: Colors.transparent, | |
), | |
headphoneIcon: ButtonIcon( | |
icon: Icon( | |
Icons.volume_down, | |
color: Colors.white, | |
size: 24.sp, | |
), | |
backgroundColor: Colors.transparent, | |
), | |
iconSize: Size(24.sp, 24.sp), | |
buttonSize: Size(40.sp, 40.sp), | |
), | |
), | |
), | |
Positioned( | |
bottom: 32.sp, | |
right: 24.sp, | |
child: Container( | |
width: 40.sp, | |
height: 40.sp, | |
decoration: ShapeDecoration( | |
color: Colors.white.withOpacity(0.20000000298023224), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(12), | |
), | |
), | |
child: ZegoToggleMicrophoneButton( | |
defaultOn: true, | |
normalIcon: ButtonIcon( | |
icon: Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: BaseImageWidget( | |
imageUri: ImageConstants.callMicIcon, | |
size: Size(24.sp, 24.sp), | |
), | |
), | |
backgroundColor: Colors.transparent, | |
), | |
offIcon: ButtonIcon( | |
icon: Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: BaseImageWidget( | |
imageUri: ImageConstants.callMuteMicIcon, | |
size: Size(24.sp, 24.sp), | |
), | |
), | |
backgroundColor: Colors.transparent, | |
), | |
iconSize: Size(24.sp, 24.sp), | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class CallCloseBottomSheetWidget extends StatelessWidget { | |
const CallCloseBottomSheetWidget({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
children: [ | |
g28Box, | |
BaseImageWidget( | |
imageUri: ImageConstants.exitIcon, | |
size: Size.square(80.sp), | |
), | |
g28Box, | |
Text( | |
'Are you sure you want to exit?', | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 18.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w700, | |
), | |
), | |
g12Box, | |
Text( | |
'Click confirm to proceed.', | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
color: const Color(0xFFCCCCCC), | |
fontSize: 14.sp, | |
fontFamily: 'Roboto', | |
fontWeight: FontWeight.w400, | |
), | |
), | |
g30Box, | |
Row( | |
children: [ | |
Expanded( | |
child: InversePrimaryBorderedButton( | |
isLoading: false, | |
label: 'Cancel', | |
onPressed: () { | |
Navigator.pop(context, false); | |
}, | |
), | |
), | |
g16Box, | |
Expanded( | |
child: PrimaryButton( | |
isLoading: false, | |
label: 'Confirm', | |
onPressed: () { | |
Navigator.pop(context, true); | |
}, | |
), | |
), | |
], | |
), | |
], | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment