commit
b22c451cc9
@ -1,18 +1,85 @@
|
|||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
extension MembershipUpdate on SyncUpdate {
|
extension MembershipUpdate on SyncUpdate {
|
||||||
List<Event> messages(Room chat) {
|
bool isMembershipUpdate(String userId) {
|
||||||
if (rooms?.join == null ||
|
return isMembershipUpdateByType(Membership.join, userId) ||
|
||||||
!rooms!.join!.containsKey(chat.id) ||
|
isMembershipUpdateByType(Membership.leave, userId) ||
|
||||||
rooms!.join![chat.id]!.timeline?.events == null) {
|
isMembershipUpdateByType(Membership.invite, userId);
|
||||||
return [];
|
}
|
||||||
|
|
||||||
|
bool isMembershipUpdateByType(Membership type, String userId) {
|
||||||
|
final List<SyncRoomUpdate>? updates = getRoomUpdates(type);
|
||||||
|
if (updates?.isEmpty ?? true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final SyncRoomUpdate update in updates!) {
|
||||||
|
final List<dynamic>? events = getRoomUpdateEvents(type, update);
|
||||||
|
if (hasMembershipUpdate(
|
||||||
|
events,
|
||||||
|
type.name,
|
||||||
|
userId,
|
||||||
|
)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SyncRoomUpdate>? getRoomUpdates(Membership type) {
|
||||||
|
switch (type) {
|
||||||
|
case Membership.join:
|
||||||
|
return rooms?.join?.values.toList();
|
||||||
|
case Membership.leave:
|
||||||
|
return rooms?.leave?.values.toList();
|
||||||
|
case Membership.invite:
|
||||||
|
return rooms?.invite?.values.toList();
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSpaceChildUpdate(String activeSpaceId) {
|
||||||
|
if (rooms?.join?.isEmpty ?? true) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
for (final update in rooms!.join!.entries) {
|
||||||
|
final String spaceId = update.key;
|
||||||
|
final List<MatrixEvent>? timelineEvents = update.value.timeline?.events;
|
||||||
|
final bool isUpdate = timelineEvents != null &&
|
||||||
|
spaceId == activeSpaceId &&
|
||||||
|
timelineEvents.any((event) => event.type == EventTypes.SpaceChild);
|
||||||
|
if (isUpdate) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<dynamic>? getRoomUpdateEvents(Membership type, SyncRoomUpdate update) {
|
||||||
|
switch (type) {
|
||||||
|
case Membership.join:
|
||||||
|
return (update as JoinedRoomUpdate).timeline?.events;
|
||||||
|
case Membership.leave:
|
||||||
|
return (update as LeftRoomUpdate).timeline?.events;
|
||||||
|
case Membership.invite:
|
||||||
|
return (update as InvitedRoomUpdate).inviteState;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rooms!.join![chat.id]!.timeline!.events!
|
bool hasMembershipUpdate(
|
||||||
.where(
|
List<dynamic>? events,
|
||||||
(event) => event.type == EventTypes.Message,
|
String membershipType,
|
||||||
)
|
String userId,
|
||||||
.map((event) => Event.fromMatrixEvent(event, chat))
|
) {
|
||||||
.toList();
|
if (events == null) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return events.any(
|
||||||
|
(event) =>
|
||||||
|
event.type == EventTypes.RoomMember &&
|
||||||
|
event.stateKey == userId &&
|
||||||
|
event.content['membership'] == membershipType,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fluffychat/pages/chat/chat.dart';
|
|
||||||
import 'package:fluffychat/pangea/extensions/sync_update_extension.dart';
|
|
||||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
|
||||||
import 'package:fluffychat/pangea/widgets/chat/round_timer.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:matrix/matrix.dart';
|
|
||||||
|
|
||||||
enum RoundState { notStarted, inProgress, completed }
|
|
||||||
|
|
||||||
class GameRoundModel {
|
|
||||||
static const int timerMaxSeconds = 180;
|
|
||||||
|
|
||||||
final ChatController controller;
|
|
||||||
final Completer<void> roundCompleter = Completer<void>();
|
|
||||||
late DateTime createdAt;
|
|
||||||
RoundTimer timer;
|
|
||||||
DateTime? startTime;
|
|
||||||
DateTime? endTime;
|
|
||||||
RoundState state = RoundState.notStarted;
|
|
||||||
StreamSubscription? syncSubscription;
|
|
||||||
final Set<String> messageIDs = {};
|
|
||||||
|
|
||||||
GameRoundModel({
|
|
||||||
required this.controller,
|
|
||||||
required this.timer,
|
|
||||||
}) {
|
|
||||||
createdAt = DateTime.now();
|
|
||||||
debugPrint("timeline: ${controller.room.timeline}");
|
|
||||||
syncSubscription ??= client.onSync.stream.listen((update) {
|
|
||||||
final newMessages = update.messages(controller.room);
|
|
||||||
final botMessages = newMessages
|
|
||||||
.where((msg) => msg.senderId == BotName.byEnvironment)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (botMessages.isNotEmpty &&
|
|
||||||
botMessages.any(
|
|
||||||
(msg) =>
|
|
||||||
msg.originServerTs.isAfter(createdAt) &&
|
|
||||||
!messageIDs.contains(msg.eventId),
|
|
||||||
)) {
|
|
||||||
if (state == RoundState.notStarted) {
|
|
||||||
startRound();
|
|
||||||
} else if (state == RoundState.inProgress) {
|
|
||||||
endRound();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final message in newMessages) {
|
|
||||||
if (message.originServerTs.isAfter(createdAt) &&
|
|
||||||
!messageIDs.contains(message.eventId) &&
|
|
||||||
!message.eventId.startsWith("Pangea Chat")) {
|
|
||||||
messageIDs.add(message.eventId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Client get client => controller.pangeaController.matrixState.client;
|
|
||||||
|
|
||||||
bool get isCompleted => roundCompleter.isCompleted;
|
|
||||||
|
|
||||||
void startRound() {
|
|
||||||
debugPrint("starting round");
|
|
||||||
state = RoundState.inProgress;
|
|
||||||
startTime = DateTime.now();
|
|
||||||
controller.roundTimerStateKey.currentState?.resetTimer(
|
|
||||||
roundLength: timerMaxSeconds,
|
|
||||||
);
|
|
||||||
controller.roundTimerStateKey.currentState?.startTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void endRound() {
|
|
||||||
debugPrint("ending round, message IDs: $messageIDs");
|
|
||||||
endTime = DateTime.now();
|
|
||||||
state = RoundState.completed;
|
|
||||||
controller.roundTimerStateKey.currentState?.resetTimer();
|
|
||||||
syncSubscription?.cancel();
|
|
||||||
roundCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
syncSubscription?.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// Create a timer that counts down to the given time
|
|
||||||
/// Default duration is 180 seconds
|
|
||||||
class RoundTimer extends StatefulWidget {
|
|
||||||
final int timerMaxSeconds;
|
|
||||||
final Duration roundDuration;
|
|
||||||
|
|
||||||
const RoundTimer({
|
|
||||||
super.key,
|
|
||||||
this.timerMaxSeconds = 180,
|
|
||||||
this.roundDuration = const Duration(seconds: 1),
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
RoundTimerState createState() => RoundTimerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class RoundTimerState extends State<RoundTimer> {
|
|
||||||
int currentSeconds = 0;
|
|
||||||
Timer? _timer;
|
|
||||||
bool isTiming = false;
|
|
||||||
Duration? duration;
|
|
||||||
int timerMaxSeconds = 180;
|
|
||||||
|
|
||||||
void resetTimer({Duration? roundDuration, int? roundLength}) {
|
|
||||||
if (_timer != null) {
|
|
||||||
_timer!.cancel();
|
|
||||||
isTiming = false;
|
|
||||||
}
|
|
||||||
if (roundDuration != null) {
|
|
||||||
duration = roundDuration;
|
|
||||||
}
|
|
||||||
if (roundLength != null) {
|
|
||||||
timerMaxSeconds = roundLength;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
currentSeconds = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
int get remainingTime => timerMaxSeconds - currentSeconds;
|
|
||||||
|
|
||||||
String get timerText =>
|
|
||||||
'${(remainingTime ~/ 60).toString().padLeft(2, '0')}: ${(remainingTime % 60).toString().padLeft(2, '0')}';
|
|
||||||
|
|
||||||
startTimer() {
|
|
||||||
_timer = Timer.periodic(duration ?? widget.roundDuration, (timer) {
|
|
||||||
setState(() {
|
|
||||||
currentSeconds++;
|
|
||||||
if (currentSeconds >= timerMaxSeconds) timer.cancel();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
setState(() {
|
|
||||||
isTiming = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
stopTimer() {
|
|
||||||
if (_timer != null) {
|
|
||||||
_timer!.cancel();
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
isTiming = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
duration = widget.roundDuration;
|
|
||||||
timerMaxSeconds = widget.timerMaxSeconds;
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
if (_timer != null) {
|
|
||||||
_timer!.cancel();
|
|
||||||
}
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: const Color.fromARGB(255, 126, 22, 14),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(
|
|
||||||
5,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(timerText),
|
|
||||||
// Row(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
// children: [
|
|
||||||
// IconButton(
|
|
||||||
// onPressed: isTiming ? stopTimeout : startTimeout,
|
|
||||||
// icon: Icon(isTiming ? Icons.pause_circle : Icons.play_circle),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue