✨ Better chat overlay
This commit is contained in:
parent
fd587270da
commit
688f035f85
@ -278,5 +278,8 @@
|
|||||||
"settingsHideBottomNav": "Hide Bottom Navigation",
|
"settingsHideBottomNav": "Hide Bottom Navigation",
|
||||||
"settingsSoundEffects": "Sound Effects",
|
"settingsSoundEffects": "Sound Effects",
|
||||||
"settingsAprilFoolFeatures": "April Fool Features",
|
"settingsAprilFoolFeatures": "April Fool Features",
|
||||||
"settingsEnterToSend": "Enter to Send"
|
"settingsEnterToSend": "Enter to Send",
|
||||||
|
"postTitle": "Title",
|
||||||
|
"postDescription": "Description",
|
||||||
|
"call": "Call"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/screens/chat/chat.dart';
|
import 'package:island/screens/chat/chat.dart';
|
||||||
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@ -11,6 +12,14 @@ import 'package:island/pods/websocket.dart';
|
|||||||
part 'call.g.dart';
|
part 'call.g.dart';
|
||||||
part 'call.freezed.dart';
|
part 'call.freezed.dart';
|
||||||
|
|
||||||
|
String formatDuration(Duration duration) {
|
||||||
|
String negativeSign = duration.isNegative ? '-' : '';
|
||||||
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||||
|
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60).abs());
|
||||||
|
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs());
|
||||||
|
return "$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class CallState with _$CallState {
|
sealed class CallState with _$CallState {
|
||||||
const factory CallState({
|
const factory CallState({
|
||||||
@ -18,6 +27,7 @@ sealed class CallState with _$CallState {
|
|||||||
required bool isMicrophoneEnabled,
|
required bool isMicrophoneEnabled,
|
||||||
required bool isCameraEnabled,
|
required bool isCameraEnabled,
|
||||||
required bool isScreenSharing,
|
required bool isScreenSharing,
|
||||||
|
@Default(Duration(seconds: 0)) Duration duration,
|
||||||
String? error,
|
String? error,
|
||||||
}) = _CallState;
|
}) = _CallState;
|
||||||
}
|
}
|
||||||
@ -54,6 +64,8 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
List.unmodifiable(_participants);
|
List.unmodifiable(_participants);
|
||||||
LocalParticipant? get localParticipant => _localParticipant;
|
LocalParticipant? get localParticipant => _localParticipant;
|
||||||
|
|
||||||
|
Timer? _durationTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CallState build() {
|
CallState build() {
|
||||||
// Subscribe to websocket updates
|
// Subscribe to websocket updates
|
||||||
@ -219,8 +231,16 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
|
|
||||||
Future<void> joinRoom(String roomId) async {
|
Future<void> joinRoom(String roomId) async {
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
|
if (_room != null) {
|
||||||
|
await _room!.disconnect();
|
||||||
|
await _room!.dispose();
|
||||||
|
_room = null;
|
||||||
|
_localParticipant = null;
|
||||||
|
_participants = [];
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final apiClient = ref.read(apiClientProvider);
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
|
||||||
final response = await apiClient.get('/chat/realtime/$roomId/join');
|
final response = await apiClient.get('/chat/realtime/$roomId/join');
|
||||||
if (response.statusCode == 200 && response.data != null) {
|
if (response.statusCode == 200 && response.data != null) {
|
||||||
final data = response.data;
|
final data = response.data;
|
||||||
@ -229,6 +249,19 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
final participants = joinResponse.participants;
|
final participants = joinResponse.participants;
|
||||||
final String endpoint = joinResponse.endpoint;
|
final String endpoint = joinResponse.endpoint;
|
||||||
final String token = joinResponse.token;
|
final String token = joinResponse.token;
|
||||||
|
|
||||||
|
// Setup duration timer
|
||||||
|
_durationTimer?.cancel();
|
||||||
|
_durationTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
state = state.copyWith(
|
||||||
|
duration: Duration(
|
||||||
|
milliseconds:
|
||||||
|
(DateTime.now().millisecondsSinceEpoch -
|
||||||
|
(ongoingCall?.createdAt.millisecondsSinceEpoch ?? 0)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Connect to LiveKit
|
// Connect to LiveKit
|
||||||
_room = Room();
|
_room = Room();
|
||||||
|
|
||||||
@ -314,5 +347,6 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
_roomListener?.dispose();
|
_roomListener?.dispose();
|
||||||
_room?.removeListener(_onRoomChange);
|
_room?.removeListener(_onRoomChange);
|
||||||
_room?.dispose();
|
_room?.dispose();
|
||||||
|
_durationTimer?.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$CallState {
|
mixin _$CallState {
|
||||||
|
|
||||||
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; String? get error;
|
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; Duration get duration; String? get error;
|
||||||
/// Create a copy of CallState
|
/// Create a copy of CallState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@ -26,16 +26,16 @@ $CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.error, error) || other.error == error));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,error);
|
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, error: $error)';
|
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ abstract mixin class $CallStateCopyWith<$Res> {
|
|||||||
factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl;
|
factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, String? error
|
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -63,13 +63,14 @@ class _$CallStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of CallState
|
/// Create a copy of CallState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? error = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
|
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
|
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
|
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
|
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -81,13 +82,14 @@ as String?,
|
|||||||
|
|
||||||
|
|
||||||
class _CallState implements CallState {
|
class _CallState implements CallState {
|
||||||
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, this.error});
|
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, this.duration = const Duration(seconds: 0), this.error});
|
||||||
|
|
||||||
|
|
||||||
@override final bool isConnected;
|
@override final bool isConnected;
|
||||||
@override final bool isMicrophoneEnabled;
|
@override final bool isMicrophoneEnabled;
|
||||||
@override final bool isCameraEnabled;
|
@override final bool isCameraEnabled;
|
||||||
@override final bool isScreenSharing;
|
@override final bool isScreenSharing;
|
||||||
|
@override@JsonKey() final Duration duration;
|
||||||
@override final String? error;
|
@override final String? error;
|
||||||
|
|
||||||
/// Create a copy of CallState
|
/// Create a copy of CallState
|
||||||
@ -100,16 +102,16 @@ _$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallSt
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.error, error) || other.error == error));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,error);
|
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, error: $error)';
|
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -120,7 +122,7 @@ abstract mixin class _$CallStateCopyWith<$Res> implements $CallStateCopyWith<$Re
|
|||||||
factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl;
|
factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, String? error
|
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -137,13 +139,14 @@ class __$CallStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of CallState
|
/// Create a copy of CallState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? error = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) {
|
||||||
return _then(_CallState(
|
return _then(_CallState(
|
||||||
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
|
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
|
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
|
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
|
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ part of 'call.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$callNotifierHash() => r'5512070f943d98e999d97549c73e4d5f6e7b3ddd';
|
String _$callNotifierHash() => r'0ae2e8ba21f145c80e1e65cf4fd15a7add17da78';
|
||||||
|
|
||||||
/// See also [CallNotifier].
|
/// See also [CallNotifier].
|
||||||
@ProviderFor(CallNotifier)
|
@ProviderFor(CallNotifier)
|
||||||
|
@ -52,6 +52,7 @@ class WebSocketService {
|
|||||||
Future<void> connect(Ref ref) async {
|
Future<void> connect(Ref ref) async {
|
||||||
_ref = ref;
|
_ref = ref;
|
||||||
|
|
||||||
|
_statusStreamController.sink.add(WebSocketState.connecting());
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
final baseUrl = ref.watch(serverUrlProvider);
|
||||||
final atk = await getFreshAtk(
|
final atk = await getFreshAtk(
|
||||||
ref.watch(tokenPairProvider),
|
ref.watch(tokenPairProvider),
|
||||||
|
@ -284,7 +284,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(title: const Text('Account')),
|
appBar: AppBar(title: const Text('account').tr()),
|
||||||
body:
|
body:
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 360),
|
constraints: const BoxConstraints(maxWidth: 360),
|
||||||
|
@ -17,6 +17,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didPush(Route route, Route? previousRoute) {
|
void didPush(Route route, Route? previousRoute) {
|
||||||
|
if (route is DialogRoute) return;
|
||||||
Future(() {
|
Future(() {
|
||||||
onChange(route.settings.name);
|
onChange(route.settings.name);
|
||||||
});
|
});
|
||||||
@ -24,6 +25,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didPop(Route route, Route? previousRoute) {
|
void didPop(Route route, Route? previousRoute) {
|
||||||
|
if (route is DialogRoute) return;
|
||||||
Future(() {
|
Future(() {
|
||||||
onChange(previousRoute?.settings.name);
|
onChange(previousRoute?.settings.name);
|
||||||
});
|
});
|
||||||
|
@ -5,11 +5,10 @@ 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/pods/call.dart';
|
import 'package:island/pods/call.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
|
||||||
import 'package:island/screens/chat/chat.dart';
|
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/chat/call_button.dart';
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
|
import 'package:island/widgets/chat/call_overlay.dart';
|
||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@ -22,8 +21,6 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
|
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
|
||||||
final userInfo = ref.watch(userInfoProvider);
|
|
||||||
final chatRoom = ref.watch(chatroomProvider(roomId));
|
|
||||||
final callState = ref.watch(callNotifierProvider);
|
final callState = ref.watch(callNotifierProvider);
|
||||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
|
|
||||||
@ -32,10 +29,6 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
final actionButtonStyle = ButtonStyle(
|
|
||||||
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
|
|
||||||
);
|
|
||||||
|
|
||||||
final viewMode = useState<String>('grid');
|
final viewMode = useState<String>('grid');
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
@ -74,20 +67,12 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
chatRoom.whenOrNull()?.name ?? 'loading'.tr(),
|
ongoingCall.value?.room.name ?? 'call'.tr(),
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
callState.isConnected
|
callState.isConnected
|
||||||
? Duration(
|
? formatDuration(callState.duration)
|
||||||
milliseconds:
|
|
||||||
(DateTime.now().millisecondsSinceEpoch -
|
|
||||||
(ongoingCall
|
|
||||||
.value
|
|
||||||
?.createdAt
|
|
||||||
.millisecondsSinceEpoch ??
|
|
||||||
0)),
|
|
||||||
).toString()
|
|
||||||
: 'Connecting',
|
: 'Connecting',
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
@ -131,78 +116,6 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
Card(
|
|
||||||
margin: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (callNotifier.localParticipant == null) {
|
|
||||||
return CircularProgressIndicator().center();
|
|
||||||
}
|
|
||||||
return SizedBox(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
child:
|
|
||||||
SpeakingRippleAvatar(
|
|
||||||
isSpeaking:
|
|
||||||
callNotifier
|
|
||||||
.localParticipant!
|
|
||||||
.isSpeaking,
|
|
||||||
audioLevel:
|
|
||||||
callNotifier
|
|
||||||
.localParticipant!
|
|
||||||
.audioLevel,
|
|
||||||
pictureId:
|
|
||||||
userInfo.value?.profile.pictureId,
|
|
||||||
size: 36,
|
|
||||||
).center(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
callState.isMicrophoneEnabled
|
|
||||||
? Icons.mic
|
|
||||||
: Icons.mic_off,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
callNotifier.toggleMicrophone();
|
|
||||||
},
|
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
callState.isCameraEnabled
|
|
||||||
? Icons.videocam
|
|
||||||
: Icons.videocam_off,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
callNotifier.toggleCamera();
|
|
||||||
},
|
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
callState.isScreenSharing
|
|
||||||
? Icons.stop_screen_share
|
|
||||||
: Icons.screen_share,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
callNotifier.toggleScreenShare();
|
|
||||||
},
|
|
||||||
style: actionButtonStyle,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(all: 16),
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@ -374,6 +287,8 @@ class CallScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
CallControlsBar(),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -11,6 +11,7 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/realm.dart';
|
import 'package:island/models/realm.dart';
|
||||||
|
import 'package:island/pods/call.dart';
|
||||||
import 'package:island/pods/chat_summary.dart';
|
import 'package:island/pods/chat_summary.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
@ -21,6 +22,7 @@ import 'package:island/services/responsive.dart';
|
|||||||
import 'package:island/widgets/account/account_picker.dart';
|
import 'package:island/widgets/account/account_picker.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:island/widgets/chat/call_overlay.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/realms/selection_dropdown.dart';
|
import 'package:island/widgets/realms/selection_dropdown.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
@ -147,12 +149,11 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
subtitle: buildSubtitle(),
|
subtitle: buildSubtitle(),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
// Clear unread count if there are unread messages
|
// Clear unread count if there are unread messages
|
||||||
final summary = await ref.read(chatSummaryProvider.future);
|
ref.read(chatSummaryProvider.future).then((summary) {
|
||||||
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
|
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
|
||||||
await ref
|
ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id);
|
||||||
.read(chatSummaryProvider.notifier)
|
|
||||||
.clearUnreadCount(room.id);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
onTap?.call();
|
onTap?.call();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -213,6 +214,8 @@ class ChatListScreen extends HookConsumerWidget {
|
|||||||
0,
|
0,
|
||||||
); // 0 for All, 1 for Direct Messages, 2 for Group Chats
|
); // 0 for All, 1 for Direct Messages, 2 for Group Chats
|
||||||
|
|
||||||
|
final callState = ref.watch(callNotifierProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
tabController.addListener(() {
|
tabController.addListener(() {
|
||||||
selectedTab.value = tabController.index;
|
selectedTab.value = tabController.index;
|
||||||
@ -334,7 +337,9 @@ class ChatListScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: const Icon(Symbols.add),
|
child: const Icon(Symbols.add),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
@ -354,7 +359,10 @@ class ChatListScreen extends HookConsumerWidget {
|
|||||||
ref.invalidate(chatroomsJoinedProvider);
|
ref.invalidate(chatroomsJoinedProvider);
|
||||||
}),
|
}),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.zero,
|
padding:
|
||||||
|
callState.isConnected
|
||||||
|
? EdgeInsets.only(bottom: 96)
|
||||||
|
: EdgeInsets.zero,
|
||||||
itemCount:
|
itemCount:
|
||||||
items
|
items
|
||||||
.where(
|
.where(
|
||||||
@ -362,7 +370,8 @@ class ChatListScreen extends HookConsumerWidget {
|
|||||||
selectedTab.value == 0 ||
|
selectedTab.value == 0 ||
|
||||||
(selectedTab.value == 1 &&
|
(selectedTab.value == 1 &&
|
||||||
item.type == 1) ||
|
item.type == 1) ||
|
||||||
(selectedTab.value == 2 && item.type != 1),
|
(selectedTab.value == 2 &&
|
||||||
|
item.type != 1),
|
||||||
)
|
)
|
||||||
.length,
|
.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@ -388,14 +397,17 @@ class ChatListScreen extends HookConsumerWidget {
|
|||||||
ChatRoomRoute(id: item.id),
|
ChatRoomRoute(id: item.id),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
context.router.push(ChatRoomRoute(id: item.id));
|
context.router.push(
|
||||||
|
ChatRoomRoute(id: item.id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading:
|
||||||
|
() => const Center(child: CircularProgressIndicator()),
|
||||||
error:
|
error:
|
||||||
(error, stack) => ResponseErrorWidget(
|
(error, stack) => ResponseErrorWidget(
|
||||||
error: error,
|
error: error,
|
||||||
@ -407,6 +419,14 @@ class ChatListScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: const CallOverlayBar().padding(horizontal: 16, vertical: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import 'package:island/screens/posts/compose.dart';
|
|||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:island/widgets/chat/call_overlay.dart';
|
||||||
import 'package:island/widgets/chat/message_item.dart';
|
import 'package:island/widgets/chat/message_item.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
@ -352,6 +353,10 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
if (message.chatRoomId != chatRoom.value?.id) return;
|
if (message.chatRoomId != chatRoom.value?.id) return;
|
||||||
switch (pkt.type) {
|
switch (pkt.type) {
|
||||||
case 'messages.new':
|
case 'messages.new':
|
||||||
|
if (message.type.startsWith('call')) {
|
||||||
|
// Handle the ongoing call.
|
||||||
|
ref.invalidate(ongoingCallProvider(message.chatRoomId));
|
||||||
|
}
|
||||||
messagesNotifier.receiveMessage(message);
|
messagesNotifier.receiveMessage(message);
|
||||||
// Send read receipt for new message
|
// Send read receipt for new message
|
||||||
sendReadReceipt();
|
sendReadReceipt();
|
||||||
@ -525,7 +530,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: messages.when(
|
child: messages.when(
|
||||||
@ -536,7 +543,8 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
: SuperListView.builder(
|
: SuperListView.builder(
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
reverse: true, // Show newest messages at the bottom
|
reverse:
|
||||||
|
true, // Show newest messages at the bottom
|
||||||
itemCount: messageList.length,
|
itemCount: messageList.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final message = messageList[index];
|
final message = messageList[index];
|
||||||
@ -546,7 +554,8 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
: null;
|
: null;
|
||||||
final isLastInGroup =
|
final isLastInGroup =
|
||||||
nextMessage == null ||
|
nextMessage == null ||
|
||||||
nextMessage.senderId != message.senderId ||
|
nextMessage.senderId !=
|
||||||
|
message.senderId ||
|
||||||
nextMessage.createdAt
|
nextMessage.createdAt
|
||||||
.difference(message.createdAt)
|
.difference(message.createdAt)
|
||||||
.inMinutes
|
.inMinutes
|
||||||
@ -594,7 +603,8 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
progress:
|
progress:
|
||||||
attachmentProgress.value[message.id],
|
attachmentProgress.value[message
|
||||||
|
.id],
|
||||||
showAvatar: isLastInGroup,
|
showAvatar: isLastInGroup,
|
||||||
),
|
),
|
||||||
loading:
|
loading:
|
||||||
@ -609,7 +619,8 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading:
|
||||||
|
() => const Center(child: CircularProgressIndicator()),
|
||||||
error:
|
error:
|
||||||
(error, _) => ResponseErrorWidget(
|
(error, _) => ResponseErrorWidget(
|
||||||
error: error,
|
error: error,
|
||||||
@ -674,6 +685,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
child: CallOverlayBar().padding(horizontal: 8, top: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,7 +316,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: titleController,
|
controller: titleController,
|
||||||
decoration: InputDecoration.collapsed(
|
decoration: InputDecoration.collapsed(
|
||||||
hintText: 'Title',
|
hintText: 'title'.tr(),
|
||||||
),
|
),
|
||||||
style: TextStyle(fontSize: 16),
|
style: TextStyle(fontSize: 16),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
@ -326,7 +326,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: descriptionController,
|
controller: descriptionController,
|
||||||
decoration: InputDecoration.collapsed(
|
decoration: InputDecoration.collapsed(
|
||||||
hintText: 'Description',
|
hintText: 'description'.tr(),
|
||||||
),
|
),
|
||||||
style: TextStyle(fontSize: 16),
|
style: TextStyle(fontSize: 16),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
|
@ -13,8 +13,6 @@ import 'package:island/services/responsive.dart';
|
|||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:island/widgets/chat/call_overlay.dart';
|
|
||||||
import 'package:island/pods/call.dart';
|
|
||||||
|
|
||||||
class WindowScaffold extends HookConsumerWidget {
|
class WindowScaffold extends HookConsumerWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@ -152,22 +150,8 @@ class AppScaffold extends StatelessWidget {
|
|||||||
noBackground
|
noBackground
|
||||||
? Colors.transparent
|
? Colors.transparent
|
||||||
: Theme.of(context).scaffoldBackgroundColor,
|
: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Stack(
|
body:
|
||||||
children: [
|
noBackground ? content : AppBackground(isRoot: true, child: content),
|
||||||
SizedBox.expand(
|
|
||||||
child:
|
|
||||||
noBackground
|
|
||||||
? content
|
|
||||||
: AppBackground(isRoot: true, child: content),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
bottom: 8,
|
|
||||||
child: const _GlobalCallOverlay(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
bottomNavigationBar: bottomNavigationBar,
|
bottomNavigationBar: bottomNavigationBar,
|
||||||
bottomSheet: bottomSheet,
|
bottomSheet: bottomSheet,
|
||||||
@ -206,23 +190,6 @@ class PageBackButton extends StatelessWidget {
|
|||||||
|
|
||||||
const kAppBackgroundImagePath = 'island_app_background';
|
const kAppBackgroundImagePath = 'island_app_background';
|
||||||
|
|
||||||
/// Global call overlay bar (appears when in a call but not on the call screen)
|
|
||||||
class _GlobalCallOverlay extends HookConsumerWidget {
|
|
||||||
const _GlobalCallOverlay();
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final callState = ref.watch(callNotifierProvider);
|
|
||||||
// Find current route name
|
|
||||||
final modalRoute = ModalRoute.of(context);
|
|
||||||
final isOnCallScreen = modalRoute?.settings.name?.contains('call') ?? false;
|
|
||||||
// You may want to store roomId in callState for more robust navigation
|
|
||||||
if (callState.isConnected && !isOnCallScreen) {
|
|
||||||
return CallOverlayBar();
|
|
||||||
}
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final backgroundImageFileProvider = FutureProvider<File?>((ref) async {
|
final backgroundImageFileProvider = FutureProvider<File?>((ref) async {
|
||||||
if (kIsWeb) return null;
|
if (kIsWeb) return null;
|
||||||
final dir = await getApplicationSupportDirectory();
|
final dir = await getApplicationSupportDirectory();
|
||||||
|
@ -1,10 +1,94 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/call.dart';
|
import 'package:island/pods/call.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/route.gr.dart';
|
import 'package:island/route.gr.dart';
|
||||||
|
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class CallControlsBar extends HookConsumerWidget {
|
||||||
|
const CallControlsBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final callState = ref.watch(callNotifierProvider);
|
||||||
|
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||||
|
|
||||||
|
final userInfo = ref.watch(userInfoProvider);
|
||||||
|
|
||||||
|
final actionButtonStyle = ButtonStyle(
|
||||||
|
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (callNotifier.localParticipant == null) {
|
||||||
|
return CircularProgressIndicator().center();
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child:
|
||||||
|
SpeakingRippleAvatar(
|
||||||
|
isSpeaking:
|
||||||
|
callNotifier.localParticipant!.isSpeaking,
|
||||||
|
audioLevel:
|
||||||
|
callNotifier.localParticipant!.audioLevel,
|
||||||
|
pictureId: userInfo.value?.profile.pictureId,
|
||||||
|
size: 36,
|
||||||
|
).center(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
callNotifier.toggleMicrophone();
|
||||||
|
},
|
||||||
|
style: actionButtonStyle,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
callNotifier.toggleCamera();
|
||||||
|
},
|
||||||
|
style: actionButtonStyle,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
callState.isScreenSharing
|
||||||
|
? Icons.stop_screen_share
|
||||||
|
: Icons.screen_share,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
callNotifier.toggleScreenShare();
|
||||||
|
},
|
||||||
|
style: actionButtonStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A floating bar that appears when user is in a call but not on the call screen.
|
|
||||||
class CallOverlayBar extends HookConsumerWidget {
|
class CallOverlayBar extends HookConsumerWidget {
|
||||||
const CallOverlayBar({super.key});
|
const CallOverlayBar({super.key});
|
||||||
|
|
||||||
@ -15,48 +99,123 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
// Only show if connected and not on the call screen
|
// Only show if connected and not on the call screen
|
||||||
if (!callState.isConnected) return const SizedBox.shrink();
|
if (!callState.isConnected) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Positioned(
|
final lastSpeaker =
|
||||||
left: 16,
|
callNotifier.participants
|
||||||
right: 16,
|
.where(
|
||||||
bottom: 32,
|
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||||
child: GestureDetector(
|
)
|
||||||
|
.isEmpty
|
||||||
|
? callNotifier.participants.first
|
||||||
|
: callNotifier.participants
|
||||||
|
.where(
|
||||||
|
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||||
|
)
|
||||||
|
.fold(
|
||||||
|
callNotifier.participants.first,
|
||||||
|
(value, element) =>
|
||||||
|
element.remoteParticipant.lastSpokeAt != null &&
|
||||||
|
(value.remoteParticipant.lastSpokeAt == null ||
|
||||||
|
element.remoteParticipant.lastSpokeAt!
|
||||||
|
.compareTo(
|
||||||
|
value
|
||||||
|
.remoteParticipant
|
||||||
|
.lastSpokeAt!,
|
||||||
|
) >
|
||||||
|
0)
|
||||||
|
? element
|
||||||
|
: value,
|
||||||
|
);
|
||||||
|
|
||||||
|
final actionButtonStyle = ButtonStyle(
|
||||||
|
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (callNotifier.localParticipant == null) {
|
||||||
|
return CircularProgressIndicator().center();
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child:
|
||||||
|
SpeakingRippleAvatar(
|
||||||
|
isSpeaking: lastSpeaker.isSpeaking,
|
||||||
|
audioLevel:
|
||||||
|
lastSpeaker.remoteParticipant.audioLevel,
|
||||||
|
pictureId:
|
||||||
|
lastSpeaker
|
||||||
|
.participant
|
||||||
|
.profile
|
||||||
|
?.account
|
||||||
|
.profile
|
||||||
|
.pictureId,
|
||||||
|
size: 36,
|
||||||
|
).center(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
lastSpeaker.participant.profile?.account.nick ??
|
||||||
|
'unknown'.tr(),
|
||||||
|
).bold(),
|
||||||
|
Text(
|
||||||
|
formatDuration(callState.duration),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
callNotifier.toggleMicrophone();
|
||||||
|
},
|
||||||
|
style: actionButtonStyle,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
callNotifier.toggleCamera();
|
||||||
|
},
|
||||||
|
style: actionButtonStyle,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
callState.isScreenSharing
|
||||||
|
? Icons.stop_screen_share
|
||||||
|
: Icons.screen_share,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
callNotifier.toggleScreenShare();
|
||||||
|
},
|
||||||
|
style: actionButtonStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 16),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (callNotifier.roomId == null) return;
|
|
||||||
context.router.push(CallRoute(roomId: callNotifier.roomId!));
|
context.router.push(CallRoute(roomId: callNotifier.roomId!));
|
||||||
},
|
},
|
||||||
child: Material(
|
|
||||||
elevation: 8,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.call, color: Colors.white),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Text(
|
|
||||||
'In call',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/database/message.dart';
|
import 'package:island/database/message.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/pods/call.dart';
|
||||||
import 'package:island/screens/chat/room.dart';
|
import 'package:island/screens/chat/room.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/cloud_file_collection.dart';
|
import 'package:island/widgets/content/cloud_file_collection.dart';
|
||||||
@ -429,14 +430,6 @@ class _MessageContentCall extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String formatDuration(Duration duration) {
|
|
||||||
final hours = duration.inHours;
|
|
||||||
final minutes = duration.inMinutes.remainder(60);
|
|
||||||
final seconds = duration.inSeconds.remainder(60);
|
|
||||||
return '${hours == 0 ? '' : '$hours hours '}'
|
|
||||||
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
|
||||||
}
|
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user