mobileCall function
- CallController c,
- BuildContext context
Returns a mobile design of a CallView.
Implementation
Widget mobileCall(CallController c, BuildContext context) {
final style = Theme.of(context).style;
return LayoutBuilder(
builder: (context, constraints) {
final bool isOutgoing =
(c.outgoing || c.state.value == OngoingCallState.local) && !c.started;
// Call stackable content.
List<Widget> content = [
const SvgImage.asset(
'assets/images/background_dark.svg',
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
];
// Layer of [Widget]s to display above the UI.
List<Widget> overlay = [];
// Active call.
if ((c.isGroup && isOutgoing) ||
c.state.value == OngoingCallState.active) {
content.addAll([
Obx(() {
if (c.isDialog &&
c.primary.length == 1 &&
c.secondary.length == 1) {
return FloatingFit<Participant>(
primary: c.primary.first,
panel: c.secondary.first,
onSwapped: (p, _) => c.center(p),
fit: !c.minimized.isFalse,
intersection: c.dockRect,
onManipulated: (bool m) => c.secondaryManipulated.value = m,
itemBuilder: (e) {
return Stack(
children: [
const ParticipantDecoratorWidget(),
IgnorePointer(
child: ParticipantWidget(
e,
offstageUntilDetermined: true,
),
),
],
);
},
overlayBuilder: (e) {
return Obx(() {
final bool? muted =
e.member.owner == MediaOwnerKind.local
? !c.audioState.value.isEnabled
: null;
// TODO: Implement opened context menu detection for
// `hovered` indicator.
return ParticipantOverlayWidget(
e,
muted: muted,
hovered: false,
preferBackdrop: !c.minimized.value,
);
});
},
);
}
final Participant? center =
c.secondary.isNotEmpty ? c.primary.firstOrNull : null;
return SwappableFit<Participant>(
items: [...c.primary, ...c.secondary],
center: center,
fit: c.minimized.value,
itemBuilder: (e) {
return Obx(() {
final bool? muted =
e.member.owner == MediaOwnerKind.local
? !c.audioState.value.isEnabled
: null;
return ContextMenuRegion(
actions: [
if (c.primary.length + c.secondary.length > 1) ...[
if (center == e)
ContextMenuButton(
label: 'btn_call_uncenter'.l10n,
onPressed: c.focusAll,
trailing: const SvgIcon(SvgIcons.uncenterVideo),
)
else
ContextMenuButton(
label: 'btn_call_center'.l10n,
onPressed: () => c.center(e),
trailing: const SvgIcon(SvgIcons.centerVideo),
),
],
if (e.member.id != c.me.id) ...[
if (e.video.value?.direction.value.isEmitting ?? false)
ContextMenuButton(
label:
e.video.value?.renderer.value != null
? 'btn_call_disable_video'.l10n
: 'btn_call_enable_video'.l10n,
onPressed: () => c.toggleVideoEnabled(e),
trailing: SvgIcon(
e.video.value?.renderer.value != null
? SvgIcons.incomingVideoOn
: SvgIcons.incomingVideoOff,
),
),
if (e.audio.value?.direction.value.isEmitting ?? false)
ContextMenuButton(
label:
(e.audio.value?.direction.value.isEnabled ==
true)
? 'btn_call_disable_audio'.l10n
: 'btn_call_enable_audio'.l10n,
onPressed: () => c.toggleAudioEnabled(e),
trailing: SvgIcon(
e.audio.value?.renderer.value != null
? SvgIcons.incomingAudioOn
: SvgIcons.incomingAudioOff,
),
),
if (e.member.isDialing.isFalse)
ContextMenuButton(
label: 'btn_call_remove_participant'.l10n,
trailing: const SvgIcon(SvgIcons.removeFromCall),
onPressed:
() =>
c.removeChatCallMember(e.member.id.userId),
),
] else ...[
ContextMenuButton(
label:
c.videoState.value.isEnabled
? 'btn_call_video_off'.l10n
: 'btn_call_video_on'.l10n,
onPressed: c.toggleVideo,
trailing: SvgIcon(
c.videoState.value.isEnabled
? SvgIcons.cameraOn
: SvgIcons.cameraOff,
),
),
ContextMenuButton(
label:
c.audioState.value.isEnabled
? 'btn_call_audio_off'.l10n
: 'btn_call_audio_on'.l10n,
onPressed: c.toggleAudio,
trailing: SvgIcon(
c.audioState.value.isEnabled
? SvgIcons.micOn
: SvgIcons.micOff,
),
),
],
],
unconstrained: true,
builder: (animated) {
return AnimatedParticipant(
e,
muted: muted,
rounded: animated,
withBlur: !c.minimized.value,
);
},
);
});
},
);
}),
]);
} else {
// Call is not active.
content.add(
Obx(() {
RtcVideoRenderer? local =
(c.locals.firstOrNull?.video.value?.renderer.value ??
c.paneled.firstOrNull?.video.value?.renderer.value)
as RtcVideoRenderer?;
if (c.videoState.value != LocalTrackState.disabled &&
local != null) {
return RtcVideoView(local, fit: BoxFit.cover);
}
return Stack(
children: [
// Display a [CallCover] of the call.
Obx(() {
final bool isDialog =
c.chat.value?.chat.value.isDialog == true;
if (isDialog) {
final RxUser? user =
c.chat.value?.members.values
.firstWhereOrNull(
(e) => e.user.id != c.me.id.userId,
)
?.user;
return CallCoverWidget(c.chat.value?.callCover, user: user);
} else {
if (c.chat.value?.avatar.value != null) {
final Avatar avatar = c.chat.value!.avatar.value!;
return CallCoverWidget(
UserCallCover(
full: avatar.full,
original: avatar.original,
square: avatar.full,
vertical: avatar.full,
),
);
} else {
return CallCoverWidget(null, chat: c.chat.value);
}
}
}),
],
);
}),
);
}
// If there's any notifications to show, display them.
overlay.add(
Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(top: 8 + context.mediaQueryPadding.top),
child: Obx(() {
if (c.notifications.isEmpty) {
return const SizedBox();
}
return Column(
mainAxisSize: MainAxisSize.min,
children:
c.notifications.reversed.take(3).map((e) {
return CallNotificationWidget(
e,
onClose: () => c.notifications.remove(e),
);
}).toList(),
);
}),
),
),
);
Widget padding(Widget child) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Center(child: child),
);
Widget buttons(List<Widget> children) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: children.map((e) => Expanded(child: e)).toList(),
),
);
final List<Widget> ui = [
// Dimmed container if any video is displayed while calling.
Obx(() {
return IgnorePointer(
child: SafeAnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child:
(c.state.value != OngoingCallState.active &&
c.state.value != OngoingCallState.joining &&
([...c.primary, ...c.secondary].firstWhereOrNull(
(e) => e.video.value?.renderer.value != null,
) !=
null) &&
!c.minimized.value)
? Container(color: style.colors.onBackgroundOpacity27)
: null,
),
);
}),
// Listen to the taps only if the call is not minimized.
Obx(() {
return c.minimized.isTrue
? Container()
: Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (d) {
c.downAt = DateTime.now();
c.downPosition = d.localPosition;
c.downButtons = d.buttons;
},
onPointerUp: (d) {
if (c.secondaryManipulated.isTrue) return;
if (c.downButtons & kPrimaryButton != 0) {
if (c.state.value == OngoingCallState.active) {
final distance =
(d.localPosition.distanceSquared -
c.downPosition.distanceSquared)
.abs() <=
80000;
final time =
DateTime.now().difference(c.downAt!).inMilliseconds <
340;
if (distance && time) {
if (c.showUi.isFalse) {
c.keepUi();
} else {
c.keepUi(c.isPanelOpen.value);
}
}
}
}
},
);
}),
// Sliding from the top title bar.
CustomSafeArea(
child: Obx(() {
final bool active = c.state.value == OngoingCallState.active;
final bool incoming = !isOutgoing;
final bool showUi =
(!c.isGroup || incoming) && !active && !c.minimized.value;
return AnimatedSlider(
duration: const Duration(milliseconds: 400),
isOpen: showUi,
beginOffset: Offset(0, -190 - MediaQuery.of(context).padding.top),
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(
left: 10,
right: 10,
top: c.size.height * 0.05,
),
child: callTitle(c),
),
),
);
}),
),
// Sliding from the top call information.
CustomSafeArea(
child: Obx(() {
final bool active = c.state.value == OngoingCallState.active;
final bool showUi = c.showUi.value && active && !c.minimized.value;
return Align(
alignment: Alignment.topCenter,
child: AnimatedSlider(
duration: const Duration(milliseconds: 250),
isOpen: showUi,
beginOffset: Offset(
0,
-50 - MediaQuery.of(context).padding.top,
),
endOffset: const Offset(0, 0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(11),
boxShadow: [
CustomBoxShadow(
color: style.colors.onBackgroundOpacity20,
blurRadius: 8,
blurStyle: BlurStyle.outer,
),
],
),
margin: const EdgeInsets.fromLTRB(10, 5, 10, 2),
child: Container(
decoration: BoxDecoration(
color: style.colors.primaryAuxiliaryOpacity25,
borderRadius: BorderRadius.circular(11),
),
padding: const EdgeInsets.fromLTRB(10, 8, 10, 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
c.chat.value?.title ?? ('dot'.l10n * 3),
style: style.fonts.small.regular.onPrimary,
overflow: TextOverflow.ellipsis,
),
),
Container(
margin: const EdgeInsets.fromLTRB(7, 0, 5, 0),
width: 1,
height: 14,
color: style.colors.onPrimary,
),
if (c.isGroup) ...[
Text(
'label_a_of_b'.l10nfmt({
'a':
c.members.keys
.where((e) => e.deviceId != null)
.map((k) => k.userId)
.toSet()
.length,
'b': c.chat.value?.chat.value.membersCount ?? 1,
}),
style: style.fonts.small.regular.onPrimary,
overflow: TextOverflow.ellipsis,
),
Container(
margin: const EdgeInsets.fromLTRB(7, 0, 5, 0),
width: 1,
height: 14,
color: style.colors.onPrimary,
),
],
Text(
Config.disableInfiniteAnimations
? '00:00'
: c.duration.value.hhMmSs(),
style: style.fonts.small.regular.onPrimary,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
);
}),
),
// Sliding from the bottom buttons panel.
Obx(() {
final bool showUi =
(c.showUi.isTrue || c.state.value != OngoingCallState.active) &&
!c.minimized.value;
double panelHeight = 0;
double minHeight = 0;
List<Widget> panelChildren = [];
final bool panel =
(c.isGroup && isOutgoing) ||
c.state.value == OngoingCallState.active ||
c.state.value == OngoingCallState.joining;
// Populate the sliding panel height and its content.
if (panel) {
panelHeight = 260 + max(37, MediaQuery.of(context).padding.bottom);
panelHeight = min(c.size.height - 45, panelHeight);
minHeight = 95 + max(35, MediaQuery.of(context).padding.bottom);
minHeight = min(c.size.height - 45, minHeight);
panelChildren = [
const SizedBox(height: 12),
buttons([
if (PlatformUtils.isMobile)
padding(
c.videoState.value.isEnabled
? SwitchButton(c).build(expanded: c.isPanelOpen.value)
: SpeakerButton(c).build(expanded: c.isPanelOpen.value),
),
if (PlatformUtils.isDesktop)
padding(ScreenButton(c).build(expanded: c.isPanelOpen.value)),
padding(AudioButton(c).build(expanded: c.isPanelOpen.value)),
padding(VideoButton(c).build(expanded: c.isPanelOpen.value)),
padding(EndCallButton(c).build(expanded: c.isPanelOpen.value)),
]),
const SizedBox(height: 32),
buttons([
padding(ParticipantsButton(c).build(expanded: true)),
padding(HandButton(c).build(expanded: true)),
padding(RemoteAudioButton(c).build(expanded: true)),
padding(RemoteVideoButton(c).build(expanded: true)),
]),
];
}
final Widget child;
if (panel) {
child = AnimatedSlider(
beginOffset: Offset(0, minHeight),
isOpen: showUi,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutQuad,
reverseCurve: Curves.easeOutQuad,
listener:
() => Future.delayed(
Duration.zero,
() => c.dockRect.value = c.dockKey.globalPaintBounds,
),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(size: c.size),
child: SlidingUpPanel(
controller: c.panelController,
boxShadow: null,
color:
ConditionalBackdropFilter.enabled
? style.colors.primaryDarkOpacity70
: style.colors.primaryDarkOpacity90,
backdropEnabled: true,
backdropOpacity: 0,
minHeight: minHeight,
maxHeight: panelHeight,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
panel: ConditionalBackdropFilter(
key: c.dockKey,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
condition: !PlatformUtils.isIOS || !WebUtils.isSafari,
child: Column(
children: [
const SizedBox(height: 12),
Center(
child: Container(
width: 60,
height: 3,
decoration: BoxDecoration(
color: style.colors.onPrimaryOpacity50,
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 12),
Expanded(child: Column(children: panelChildren)),
],
),
),
onPanelSlide: (d) {
c.keepUi(true);
c.isPanelOpen.value = d > 0;
c.dockRect.value = c.dockKey.globalPaintBounds;
},
onPanelOpened: () {
c.keepUi(true);
c.isPanelOpen.value = true;
},
onPanelClosed: () {
c.keepUi();
c.isPanelOpen.value = false;
},
),
),
);
} else {
final List<Widget> widgets;
if (isOutgoing) {
widgets = [
if (PlatformUtils.isMobile)
padding(
c.videoState.value.isEnabled
? SwitchButton(c).build(hinted: false, opaque: true)
: SpeakerButton(c).build(hinted: false, opaque: true),
),
padding(AudioButton(c).build(hinted: false, opaque: true)),
padding(VideoButton(c).build(hinted: false, opaque: true)),
padding(CancelButton(c).build(hinted: false, opaque: true)),
];
} else {
widgets = [
padding(
AcceptAudioButton(
c,
highlight: !c.withVideo,
).build(expanded: true),
),
padding(
AcceptVideoButton(
c,
highlight: c.withVideo,
).build(expanded: true),
),
padding(DeclineButton(c).build(expanded: true)),
];
}
child = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(
bottom:
isOutgoing
? max(
0,
MediaQuery.of(context).padding.bottom - 30,
)
: max(30, MediaQuery.of(context).padding.bottom),
),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 250),
opacity: c.minimized.value ? 0 : 1,
child: buttons(widgets),
),
),
],
),
);
}
return SafeAnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: child,
);
}),
];
// Combines all the stackable content into [Scaffold].
Widget scaffold = Scaffold(
backgroundColor: style.colors.secondaryBackgroundLight,
body: Stack(
children: [
...content,
const MouseRegion(opaque: false, cursor: SystemMouseCursors.basic),
...ui.map((e) => ClipRect(child: e)),
...overlay,
],
),
);
if (c.minimized.value) {
c.applyConstraints(context);
} else {
c.applySecondaryConstraints();
}
return Obx(() {
return MinimizableView(
minimizationEnabled: !c.secondaryManipulated.value,
onInit: (animation) {
c.minimizedAnimation = animation;
animation.addListener(() {
if (c.state.value != OngoingCallState.joining &&
c.state.value != OngoingCallState.active) {
c.minimized.value = animation.value != 0;
} else {
if (animation.value != 0) {
c.keepUi(false);
}
c.minimized.value = animation.value == 1;
if (animation.value == 1 || animation.value == 0) {
c.minimizing.value = false;
} else {
c.minimizing.value = true;
}
}
});
},
onDispose: () => c.minimizedAnimation = null,
onSizeChanged: (s) {
c.width.value = s.width;
c.height.value = s.height;
},
child: Obx(() {
return IgnorePointer(ignoring: c.minimized.value, child: scaffold);
}),
);
});
},
);
}