⚡ Optimize call
This commit is contained in:
@@ -43,6 +43,7 @@ class AudioCallButton extends HookConsumerWidget {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/sphere/chat/realtime/${room.id}');
|
await apiClient.post('/sphere/chat/realtime/${room.id}');
|
||||||
|
ref.invalidate(ongoingCallProvider(room.id));
|
||||||
// Just join the room, the overlay will handle the UI
|
// Just join the room, the overlay will handle the UI
|
||||||
await callNotifier.joinRoom(room);
|
await callNotifier.joinRoom(room);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/account.dart';
|
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/chat/call.dart';
|
import 'package:island/pods/chat/call.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
@@ -18,6 +17,7 @@ import 'package:island/widgets/content/sheet.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
class CallControlsBar extends HookConsumerWidget {
|
class CallControlsBar extends HookConsumerWidget {
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
@@ -321,21 +321,66 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final callState = ref.watch(callProvider);
|
// Use selective watching to reduce rebuilds
|
||||||
|
final isConnected = ref.watch(
|
||||||
|
callProvider.select((state) => state.isConnected),
|
||||||
|
);
|
||||||
|
final duration = ref.watch(callProvider.select((state) => state.duration));
|
||||||
|
final isMicrophoneEnabled = ref.watch(
|
||||||
|
callProvider.select((state) => state.isMicrophoneEnabled),
|
||||||
|
);
|
||||||
final callNotifier = ref.read(callProvider.notifier);
|
final callNotifier = ref.read(callProvider.notifier);
|
||||||
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||||
|
|
||||||
|
// Memoize expensive computations
|
||||||
|
final lastSpeaker = useMemoized(() {
|
||||||
|
final participants = callNotifier.participants;
|
||||||
|
if (participants.isEmpty) return null;
|
||||||
|
|
||||||
|
final speakers = participants.where(
|
||||||
|
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (speakers.isEmpty) return participants.first;
|
||||||
|
|
||||||
|
return speakers.fold<CallParticipantLive?>(null, (previous, current) {
|
||||||
|
if (previous == null) return current;
|
||||||
|
return current.remoteParticipant.lastSpokeAt!.compareTo(
|
||||||
|
previous.remoteParticipant.lastSpokeAt!,
|
||||||
|
) >
|
||||||
|
0
|
||||||
|
? current
|
||||||
|
: previous;
|
||||||
|
});
|
||||||
|
}, [callNotifier.participants]);
|
||||||
|
|
||||||
|
final userInfo = ref.watch(userInfoProvider).value!;
|
||||||
|
|
||||||
|
// Memoize chat room name
|
||||||
|
final chatRoomName = useMemoized(() {
|
||||||
|
final room = callNotifier.chatRoom;
|
||||||
|
if (room == null) return 'unnamed'.tr();
|
||||||
|
return room.name ??
|
||||||
|
(room.members ?? [])
|
||||||
|
.where((element) => element.id != userInfo.id)
|
||||||
|
.map((element) => element.account.nick)
|
||||||
|
.first;
|
||||||
|
}, [callNotifier.chatRoom, userInfo]);
|
||||||
|
|
||||||
// State for overlay mode: compact or preview
|
// State for overlay mode: compact or preview
|
||||||
// Default to true (preview mode) so user sees video immediately after joining
|
// Default to true (preview mode) so user sees video immediately after joining
|
||||||
final isExpanded = useState(true);
|
final isExpanded = useState(true);
|
||||||
|
|
||||||
Widget child;
|
Widget child;
|
||||||
if (callState.isConnected) {
|
if (isConnected) {
|
||||||
child = _buildActiveCallOverlay(
|
child = _buildActiveCallOverlay(
|
||||||
context,
|
context,
|
||||||
ref,
|
ref,
|
||||||
callState,
|
duration,
|
||||||
|
isMicrophoneEnabled,
|
||||||
callNotifier,
|
callNotifier,
|
||||||
|
lastSpeaker,
|
||||||
|
chatRoomName,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
);
|
);
|
||||||
} else if (ongoingCall.value != null) {
|
} else if (ongoingCall.value != null) {
|
||||||
@@ -425,48 +470,20 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getChatRoomName(SnChatRoom? room, SnAccount currentUser) {
|
|
||||||
if (room == null) return 'unnamed'.tr();
|
|
||||||
return room.name ??
|
|
||||||
(room.members ?? [])
|
|
||||||
.where((element) => element.id != currentUser.id)
|
|
||||||
.map((element) => element.account.nick)
|
|
||||||
.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildActiveCallOverlay(
|
Widget _buildActiveCallOverlay(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
CallState callState,
|
Duration duration,
|
||||||
|
bool isMicrophoneEnabled,
|
||||||
CallNotifier callNotifier,
|
CallNotifier callNotifier,
|
||||||
|
CallParticipantLive? lastSpeaker,
|
||||||
|
String chatRoomName,
|
||||||
ValueNotifier<bool> isExpanded,
|
ValueNotifier<bool> isExpanded,
|
||||||
) {
|
) {
|
||||||
final lastSpeaker =
|
|
||||||
callNotifier.participants
|
|
||||||
.where((element) => element.remoteParticipant.lastSpokeAt != null)
|
|
||||||
.isEmpty
|
|
||||||
? callNotifier.participants.firstOrNull
|
|
||||||
: callNotifier.participants
|
|
||||||
.where((element) => element.remoteParticipant.lastSpokeAt != null)
|
|
||||||
.fold(
|
|
||||||
callNotifier.participants.firstOrNull,
|
|
||||||
(value, element) =>
|
|
||||||
element.remoteParticipant.lastSpokeAt != null &&
|
|
||||||
(value?.remoteParticipant.lastSpokeAt == null ||
|
|
||||||
element.remoteParticipant.lastSpokeAt!.compareTo(
|
|
||||||
value!.remoteParticipant.lastSpokeAt!,
|
|
||||||
) >
|
|
||||||
0)
|
|
||||||
? element
|
|
||||||
: value,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lastSpeaker == null) {
|
if (lastSpeaker == null) {
|
||||||
return const SizedBox.shrink(key: ValueKey('active_waiting'));
|
return const SizedBox.shrink(key: ValueKey('active_waiting'));
|
||||||
}
|
}
|
||||||
|
|
||||||
final userInfo = ref.watch(userInfoProvider).value!;
|
|
||||||
|
|
||||||
// Preview Mode (Expanded)
|
// Preview Mode (Expanded)
|
||||||
if (isExpanded.value) {
|
if (isExpanded.value) {
|
||||||
return Card(
|
return Card(
|
||||||
@@ -480,9 +497,9 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(_getChatRoomName(callNotifier.chatRoom, userInfo)),
|
Text(chatRoomName),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(formatDuration(callState.duration)).bold(),
|
Text(formatDuration(duration)).bold(),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
OpenContainer(
|
OpenContainer(
|
||||||
closedElevation: 0,
|
closedElevation: 0,
|
||||||
@@ -555,11 +572,11 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_getChatRoomName(callNotifier.chatRoom, userInfo),
|
chatRoomName,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
formatDuration(callState.duration),
|
formatDuration(duration),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -571,7 +588,7 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|||||||
Reference in New Issue
Block a user