chore: change activity roles event so admins can finish activity for everyone (#3665)
This commit is contained in:
parent
32688f332a
commit
c2134d2f3e
10 changed files with 175 additions and 166 deletions
|
|
@ -10,7 +10,6 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/pangea_message_reactions.dart';
|
||||
import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/activity_role_state_message.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/activity_state_event.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
|
|
@ -133,10 +132,6 @@ class Message extends StatelessWidget {
|
|||
if (event.type == PangeaEventTypes.activityPlan) {
|
||||
return ActivityStateEvent(event: event);
|
||||
}
|
||||
|
||||
if (event.type == PangeaEventTypes.activityRole) {
|
||||
return ActivityRoleStateMessage(event);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
return StateMessage(event);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import 'package:fluffychat/pangea/activity_planner/activity_participant_indicato
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_results_carousel.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
|
@ -100,14 +99,6 @@ class ActivityFinishedStatusMessageState
|
|||
}
|
||||
|
||||
Future<void> _archiveToAnalytics() async {
|
||||
final role = widget.room.activityRole(widget.room.client.userID!);
|
||||
if (role == null) {
|
||||
throw Exception(
|
||||
"Cannot archive activity without a role for user ${widget.room.client.userID!}",
|
||||
);
|
||||
}
|
||||
|
||||
role.archivedAt = DateTime.now();
|
||||
await widget.room.archiveActivity();
|
||||
await MatrixState.pangeaController.putAnalytics
|
||||
.sendActivityAnalytics(widget.room.id);
|
||||
|
|
@ -119,11 +110,12 @@ class ActivityFinishedStatusMessageState
|
|||
}
|
||||
|
||||
final roles = widget.room.activityRoles;
|
||||
return roles.where((role) {
|
||||
return widget.room.activitySummary!.summary!.participants.any(
|
||||
(p) => p.participantId == role.userId,
|
||||
);
|
||||
}).toList();
|
||||
return roles?.roles.where((role) {
|
||||
return widget.room.activitySummary!.summary!.participants.any(
|
||||
(p) => p.participantId == role.userId,
|
||||
);
|
||||
}).toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -44,10 +44,14 @@ class ActivityRoleModel {
|
|||
return other is ActivityRoleModel &&
|
||||
other.userId == userId &&
|
||||
other.role == role &&
|
||||
other.finishedAt == finishedAt;
|
||||
other.finishedAt == finishedAt &&
|
||||
other.archivedAt == archivedAt;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
userId.hashCode ^ role.hashCode ^ (finishedAt?.hashCode ?? 0);
|
||||
userId.hashCode ^
|
||||
role.hashCode ^
|
||||
(finishedAt?.hashCode ?? 0) ^
|
||||
(archivedAt?.hashCode ?? 0);
|
||||
}
|
||||
|
|
|
|||
67
lib/pangea/activity_planner/activity_roles_model.dart
Normal file
67
lib/pangea/activity_planner/activity_roles_model.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart';
|
||||
|
||||
class ActivityRolesModel {
|
||||
final Event? event;
|
||||
late List<ActivityRoleModel> _roles;
|
||||
|
||||
ActivityRolesModel({this.event, List<ActivityRoleModel>? roles}) {
|
||||
assert(
|
||||
event != null || roles != null,
|
||||
"Either event or roles must be provided",
|
||||
);
|
||||
|
||||
if (roles != null) {
|
||||
_roles = roles;
|
||||
} else {
|
||||
final rolesList = event!.content["roles"] as List<dynamic>? ?? [];
|
||||
try {
|
||||
_roles = rolesList
|
||||
.map<ActivityRoleModel>((e) => ActivityRoleModel.fromJson(e))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
_roles = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<ActivityRoleModel> get roles => _roles;
|
||||
|
||||
ActivityRoleModel? role(String userId) {
|
||||
return _roles.firstWhereOrNull((r) => r.userId == userId);
|
||||
}
|
||||
|
||||
/// If this user already has a role, replace it with the new one.
|
||||
/// Otherwise, add the new role.
|
||||
void updateRole(ActivityRoleModel role) {
|
||||
final index = _roles.indexWhere((r) => r.userId == role.userId);
|
||||
index != -1 ? _roles[index] = role : _roles.add(role);
|
||||
}
|
||||
|
||||
void finishAll() {
|
||||
for (final role in _roles) {
|
||||
if (role.isFinished) continue;
|
||||
role.finishedAt = DateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
static ActivityRolesModel get empty => ActivityRolesModel(
|
||||
roles: [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"roles": _roles.map((role) => role.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
static ActivityRolesModel fromJson(Map<String, dynamic> json) {
|
||||
final roles = (json["roles"] as List<dynamic>?)
|
||||
?.map<ActivityRoleModel>((e) => ActivityRoleModel.fromJson(e))
|
||||
.toList();
|
||||
|
||||
return ActivityRolesModel(roles: roles);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_roles_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_repo.dart';
|
||||
|
|
@ -15,6 +16,7 @@ import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
|
|||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
|
||||
extension ActivityRoomExtension on Room {
|
||||
Future<void> sendActivityPlan(
|
||||
|
|
@ -34,48 +36,83 @@ extension ActivityRoomExtension on Room {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> startActivity({
|
||||
Future<void> joinActivity({
|
||||
String? role,
|
||||
}) async {
|
||||
final currentRoles = activityRoles ?? ActivityRolesModel.empty;
|
||||
final activityRole = ActivityRoleModel(
|
||||
userId: client.userID!,
|
||||
role: role,
|
||||
);
|
||||
|
||||
currentRoles.updateRole(activityRole);
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRole,
|
||||
client.userID!,
|
||||
ActivityRoleModel(
|
||||
userId: client.userID!,
|
||||
role: role,
|
||||
).toJson(),
|
||||
"",
|
||||
currentRoles.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> continueActivity() async {
|
||||
final role = activityRole(client.userID!);
|
||||
if (role == null || !role.isFinished || role.isArchived) return;
|
||||
final currentRoles = activityRoles ?? ActivityRolesModel.empty;
|
||||
final role = currentRoles.role(client.userID!);
|
||||
if (role == null || !role.isFinished) return;
|
||||
|
||||
role.finishedAt = null;
|
||||
final syncFuture = client.waitForRoomInSync(id);
|
||||
role.finishedAt = null; // Reset finished state
|
||||
currentRoles.updateRole(role);
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRole,
|
||||
client.userID!,
|
||||
role.toJson(),
|
||||
"",
|
||||
currentRoles.toJson(),
|
||||
);
|
||||
await syncFuture;
|
||||
}
|
||||
|
||||
Future<void> finishActivity() async {
|
||||
final role = activityRole(client.userID!);
|
||||
if (role == null) return;
|
||||
if (isRoomAdmin) {
|
||||
await _finishActivityForAll();
|
||||
return;
|
||||
}
|
||||
|
||||
final currentRoles = activityRoles ?? ActivityRolesModel.empty;
|
||||
final role = currentRoles.role(client.userID!);
|
||||
if (role == null || role.isFinished) return;
|
||||
role.finishedAt = DateTime.now();
|
||||
final syncFuture = client.waitForRoomInSync(id);
|
||||
currentRoles.updateRole(role);
|
||||
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRole,
|
||||
client.userID!,
|
||||
role.toJson(),
|
||||
"",
|
||||
currentRoles.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _finishActivityForAll() async {
|
||||
final currentRoles = activityRoles ?? ActivityRolesModel.empty;
|
||||
currentRoles.finishAll();
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRole,
|
||||
"",
|
||||
currentRoles.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> archiveActivity() async {
|
||||
final currentRoles = activityRoles ?? ActivityRolesModel.empty;
|
||||
final role = currentRoles.role(client.userID!);
|
||||
if (role == null || !role.isFinished) return;
|
||||
|
||||
role.archivedAt = DateTime.now();
|
||||
currentRoles.updateRole(role);
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRole,
|
||||
"",
|
||||
currentRoles.toJson(),
|
||||
);
|
||||
await syncFuture;
|
||||
}
|
||||
|
||||
Future<void> setActivitySummary(
|
||||
|
|
@ -161,19 +198,6 @@ extension ActivityRoomExtension on Room {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> archiveActivity() async {
|
||||
final role = activityRole(client.userID!);
|
||||
if (role == null) return;
|
||||
|
||||
role.archivedAt = DateTime.now();
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRole,
|
||||
client.userID!,
|
||||
role.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
ActivityPlanModel? get activityPlan {
|
||||
final stateEvent = getState(PangeaEventTypes.activityPlan);
|
||||
if (stateEvent == null) return null;
|
||||
|
|
@ -193,26 +217,6 @@ extension ActivityRoomExtension on Room {
|
|||
}
|
||||
}
|
||||
|
||||
ActivityRoleModel? activityRole(String userId) {
|
||||
final stateEvent = getState(PangeaEventTypes.activityRole, userId);
|
||||
if (stateEvent == null) return null;
|
||||
|
||||
try {
|
||||
return ActivityRoleModel.fromJson(stateEvent.content);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": id,
|
||||
"userId": userId,
|
||||
"stateEvent": stateEvent.content,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ActivitySummaryModel? get activitySummary {
|
||||
final stateEvent = getState(PangeaEventTypes.activitySummary);
|
||||
if (stateEvent == null) return null;
|
||||
|
|
@ -232,14 +236,23 @@ extension ActivityRoomExtension on Room {
|
|||
}
|
||||
}
|
||||
|
||||
List<StrippedStateEvent> get _activityRoleEvents {
|
||||
return states[PangeaEventTypes.activityRole]?.values.toList() ?? [];
|
||||
}
|
||||
ActivityRolesModel? get activityRoles {
|
||||
final content = getState(PangeaEventTypes.activityRole)?.content;
|
||||
if (content == null) return null;
|
||||
|
||||
List<ActivityRoleModel> get activityRoles {
|
||||
return _activityRoleEvents
|
||||
.map((r) => ActivityRoleModel.fromJson(r.content))
|
||||
.toList();
|
||||
try {
|
||||
return ActivityRolesModel.fromJson(content);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": id,
|
||||
"stateEvent": content,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get showActivityChatUI {
|
||||
|
|
@ -250,32 +263,35 @@ extension ActivityRoomExtension on Room {
|
|||
|
||||
bool get isActiveInActivity {
|
||||
if (!showActivityChatUI) return false;
|
||||
final role = activityRole(client.userID!);
|
||||
final role = activityRoles?.role(client.userID!);
|
||||
return role != null && !role.isFinished;
|
||||
}
|
||||
|
||||
bool get isInactiveInActivity {
|
||||
if (!showActivityChatUI) return false;
|
||||
final role = activityRole(client.userID!);
|
||||
final role = activityRoles?.role(client.userID!);
|
||||
return role == null || role.isFinished;
|
||||
}
|
||||
|
||||
bool get hasCompletedActivity =>
|
||||
activityRole(client.userID!)?.isFinished ?? false;
|
||||
activityRoles?.role(client.userID!)?.isFinished ?? false;
|
||||
|
||||
bool get activityIsFinished {
|
||||
final roles = activityRoles.where((r) => r.userId != BotName.byEnvironment);
|
||||
return roles.isNotEmpty &&
|
||||
roles.every((r) {
|
||||
if (r.isFinished) return true;
|
||||
final roles = activityRoles?.roles.where(
|
||||
(r) => r.userId != BotName.byEnvironment,
|
||||
);
|
||||
|
||||
// if the user is in the chat (not null && membership is join),
|
||||
// then the activity is not finished for them
|
||||
final user = getParticipants().firstWhereOrNull(
|
||||
(u) => u.id == r.userId,
|
||||
);
|
||||
return user == null || user.membership != Membership.join;
|
||||
});
|
||||
if (roles == null || roles.isEmpty) return false;
|
||||
return roles.every((r) {
|
||||
if (r.isFinished) return true;
|
||||
|
||||
// if the user is in the chat (not null && membership is join),
|
||||
// then the activity is not finished for them
|
||||
final user = getParticipants().firstWhereOrNull(
|
||||
(u) => u.id == r.userId,
|
||||
);
|
||||
return user == null || user.membership != Membership.join;
|
||||
});
|
||||
}
|
||||
|
||||
int? get numberOfParticipants {
|
||||
|
|
@ -284,6 +300,9 @@ extension ActivityRoomExtension on Room {
|
|||
|
||||
int get remainingRoles {
|
||||
if (numberOfParticipants == null) return 0;
|
||||
return max(0, numberOfParticipants! - activityRoles.length);
|
||||
return max(0, numberOfParticipants! - (activityRoles?.roles.length ?? 0));
|
||||
}
|
||||
|
||||
bool get isHiddenActivityRoom =>
|
||||
activityRoles?.role(client.userID!)?.isArchived ?? false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class ActivityUnfinishedStatusMessageState
|
|||
? () {
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: widget.room.startActivity,
|
||||
future: widget.room.joinActivity,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ActivityRoleStateMessage extends StatelessWidget {
|
||||
final Event event;
|
||||
const ActivityRoleStateMessage(this.event, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final senderName = event.senderId == event.room.client.userID
|
||||
? L10n.of(context).you
|
||||
: event.senderFromMemoryOrFallback.calcDisplayname();
|
||||
|
||||
String role = L10n.of(context).participant;
|
||||
bool finished = false;
|
||||
|
||||
try {
|
||||
final roleContent = event.content['role'] as String?;
|
||||
if (roleContent != null) {
|
||||
role = roleContent;
|
||||
}
|
||||
|
||||
finished = event.content['finishedAt'] != null;
|
||||
} catch (e) {
|
||||
// If the role is not found, we keep the default participant role.
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Material(
|
||||
color: theme.colorScheme.surface.withAlpha(128),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 3),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
child: Text(
|
||||
finished
|
||||
? L10n.of(context).finishedTheActivity(senderName)
|
||||
: L10n.of(context).joinedTheActivity(senderName, role),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12 * AppConfig.fontSizeFactor,
|
||||
decoration:
|
||||
event.redacted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ class ActivityStateEvent extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
try {
|
||||
final activity = ActivityPlanModel.fromJson(event.content);
|
||||
final roles = event.room.activityRoles;
|
||||
final roles = event.room.activityRoles?.roles ?? [];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
|
|
@ -37,7 +37,7 @@ class ActivityStateEvent extends StatelessWidget {
|
|||
Wrap(
|
||||
spacing: 12.0,
|
||||
runSpacing: 12.0,
|
||||
children: event.room.activityRoles.map((role) {
|
||||
children: roles.map((role) {
|
||||
return ActivityParticipantIndicator(
|
||||
role: role,
|
||||
displayname: role.userId.localpart,
|
||||
|
|
|
|||
|
|
@ -35,8 +35,5 @@ extension RoomInformationRoomExtension on Room {
|
|||
getState(EventTypes.RoomCreate)?.content.tryGet<String>('type') ==
|
||||
PangeaRoomTypes.analytics;
|
||||
|
||||
bool get isHiddenActivityRoom =>
|
||||
activityRole(client.userID!)?.isArchived ?? false;
|
||||
|
||||
bool get isHiddenRoom => isAnalyticsRoom || isHiddenActivityRoom;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ extension IsStateExtension on Event {
|
|||
isEventTypeKnown ||
|
||||
[
|
||||
PangeaEventTypes.activityPlan,
|
||||
PangeaEventTypes.activityRole,
|
||||
].contains(type);
|
||||
|
||||
// we're filtering out some state events that we don't want to render
|
||||
|
|
@ -70,7 +69,6 @@ extension IsStateExtension on Event {
|
|||
EventTypes.RoomTombstone,
|
||||
EventTypes.CallInvite,
|
||||
PangeaEventTypes.activityPlan,
|
||||
PangeaEventTypes.activityRole,
|
||||
};
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue