Skip to content

Instantly share code, notes, and snippets.

@DhruvamUnikon
Created July 2, 2024 13:04
Show Gist options
  • Save DhruvamUnikon/0d7b8735751acdce42cdd564c966c8f6 to your computer and use it in GitHub Desktop.
Save DhruvamUnikon/0d7b8735751acdce42cdd564c966c8f6 to your computer and use it in GitHub Desktop.
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