Merge branch 'main' into toolbar-location-adjustments
This commit is contained in:
commit
9a56fe42b7
40 changed files with 1076 additions and 1255 deletions
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
|
|
@ -189,7 +189,7 @@ jobs:
|
|||
name: web
|
||||
path: build/web
|
||||
- name: Set up AWS CLI
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_e
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
|
|
@ -294,10 +293,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
bool showPermissionsError = false;
|
||||
// #Pangea
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
scrollController.addListener(_updateScrollController);
|
||||
|
|
@ -327,31 +322,13 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
context,
|
||||
() => Future.delayed(
|
||||
Duration.zero,
|
||||
() => setState(
|
||||
() {},
|
||||
),
|
||||
() => setState(() {}),
|
||||
),
|
||||
);
|
||||
}
|
||||
await Matrix.of(context).client.roomsLoading;
|
||||
choreographer.setRoomId(roomId);
|
||||
choreographer.messageOptions.resetSelectedDisplayLang();
|
||||
choreographer.stateListener.stream.listen((event) {
|
||||
debugPrint("chat.dart choreo event $event");
|
||||
setState(() {});
|
||||
});
|
||||
showPermissionsError = !pangeaController.permissionsController
|
||||
.isToolEnabled(ToolSetting.interactiveTranslator, room) ||
|
||||
!pangeaController.permissionsController
|
||||
.isToolEnabled(ToolSetting.interactiveGrammar, room);
|
||||
});
|
||||
|
||||
Future.delayed(
|
||||
const Duration(seconds: 5),
|
||||
() {
|
||||
if (mounted) setState(() => showPermissionsError = false);
|
||||
},
|
||||
);
|
||||
// Pangea#
|
||||
_tryLoadTimeline();
|
||||
if (kIsWeb) {
|
||||
|
|
@ -440,7 +417,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
onInsert: onInsert,
|
||||
);
|
||||
// #Pangea
|
||||
if (visibleEvents.length < 10) {
|
||||
if (visibleEvents.length < 10 && timeline != null) {
|
||||
int prevNumEvents = timeline!.events.length;
|
||||
await requestHistory();
|
||||
int numRequests = 0;
|
||||
|
|
@ -602,10 +579,9 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
});
|
||||
|
||||
// #Pangea
|
||||
final List<String> edittingEvents = [];
|
||||
void clearEdittingEvent(String eventId) {
|
||||
edittingEvents.remove(eventId);
|
||||
setState(() {});
|
||||
Event? pangeaEditingEvent;
|
||||
void clearEditingEvent() {
|
||||
pangeaEditingEvent = null;
|
||||
}
|
||||
|
||||
// Future<void> send() async {
|
||||
|
|
@ -665,11 +641,9 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
.then(
|
||||
(String? msgEventId) async {
|
||||
// #Pangea
|
||||
setState(() {
|
||||
if (previousEdit != null) {
|
||||
edittingEvents.add(previousEdit.eventId);
|
||||
}
|
||||
});
|
||||
if (previousEdit != null) {
|
||||
pangeaEditingEvent = previousEdit;
|
||||
}
|
||||
|
||||
GoogleAnalytics.sendMessage(
|
||||
room.id,
|
||||
|
|
@ -1262,9 +1236,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
void clearSelectedEvents() => setState(() {
|
||||
selectedEvents.clear();
|
||||
showEmojiPicker = false;
|
||||
//#Pangea
|
||||
choreographer.messageOptions.resetSelectedDisplayLang();
|
||||
//Pangea#
|
||||
});
|
||||
|
||||
void clearSingleSelectedEvent() {
|
||||
|
|
@ -1336,19 +1307,19 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// Pangea#
|
||||
if (!event.redacted) {
|
||||
// #Pangea
|
||||
// If previous selectedEvent has same eventId, delete previous selectedEvent
|
||||
final matches =
|
||||
selectedEvents.where((e) => e.eventId == event.eventId).toList();
|
||||
// if (selectedEvents.contains(event)) {
|
||||
// setState(
|
||||
// () => selectedEvents.remove(event),
|
||||
// );
|
||||
// }
|
||||
|
||||
// If delete first selected event with the selected eventID
|
||||
final matches = selectedEvents.where((e) => e.eventId == event.eventId);
|
||||
if (matches.isNotEmpty) {
|
||||
// if (selectedEvents.contains(event)) {
|
||||
// Pangea#
|
||||
setState(
|
||||
// #Pangea
|
||||
() => selectedEvents.remove(matches.first),
|
||||
// () => selectedEvents.remove(event),
|
||||
// Pangea#
|
||||
);
|
||||
} else {
|
||||
setState(() => selectedEvents.remove(matches.first));
|
||||
}
|
||||
// Pangea#
|
||||
else {
|
||||
setState(
|
||||
() => selectedEvents.add(event),
|
||||
);
|
||||
|
|
@ -1557,35 +1528,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
});
|
||||
|
||||
// #Pangea
|
||||
double? availableSpace;
|
||||
double? inputRowSize;
|
||||
bool? lastState;
|
||||
bool get isRowScrollable {
|
||||
if (availableSpace == null || inputRowSize == null) {
|
||||
if (lastState == null) {
|
||||
lastState = false;
|
||||
Future.delayed(Duration.zero, () {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const double offSetValue = 10;
|
||||
final bool currentState = inputRowSize! > (availableSpace! - offSetValue);
|
||||
if (!lastState! && currentState) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
if (lastState! && !currentState) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
lastState = currentState;
|
||||
return currentState;
|
||||
}
|
||||
|
||||
final Map<String, PangeaMessageEvent> _pangeaMessageEvents = {};
|
||||
final Map<String, ToolbarDisplayController> _toolbarDisplayControllers = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -170,8 +170,6 @@ class ChatEventList extends StatelessWidget {
|
|||
controller.scrollToEventId(eventId),
|
||||
longPressSelect: controller.selectedEvents.isNotEmpty,
|
||||
// #Pangea
|
||||
selectedDisplayLang:
|
||||
controller.choreographer.messageOptions.selectedDisplayLang,
|
||||
immersionMode: controller.choreographer.immersionMode,
|
||||
definitions: controller.choreographer.definitionsEnabled,
|
||||
controller: controller,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:fluffychat/config/app_config.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/input_bar_wrapper.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -12,7 +13,6 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import '../../config/themes.dart';
|
||||
import 'chat.dart';
|
||||
import 'input_bar.dart';
|
||||
|
||||
class ChatInputRow extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
|
@ -322,7 +322,10 @@ class ChatInputRow extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
||||
child: InputBar(
|
||||
// #Pangea
|
||||
// child: InputBar(
|
||||
child: InputBarWrapper(
|
||||
// Pangea#
|
||||
room: controller.room,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,9 @@ import 'package:fluffychat/pages/chat/chat_event_list.dart';
|
|||
import 'package:fluffychat/pages/chat/pinned_events.dart';
|
||||
import 'package:fluffychat/pages/chat/reactions_picker.dart';
|
||||
import 'package:fluffychat/pages/chat/reply_display.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_analytics/measure_able.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
|
|
@ -266,32 +264,20 @@ class ChatView extends StatelessWidget {
|
|||
// #Pangea
|
||||
// floatingActionButton: controller.showScrollDownButton &&
|
||||
// controller.selectedEvents.isEmpty
|
||||
floatingActionButton: controller.selectedEvents.isEmpty
|
||||
? (controller.showScrollDownButton
|
||||
// Pangea#
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 56.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: controller.scrollDown,
|
||||
heroTag: null,
|
||||
mini: true,
|
||||
child: const Icon(Icons.arrow_downward_outlined),
|
||||
),
|
||||
)
|
||||
// #Pangea
|
||||
: controller.choreographer.errorService.error != null
|
||||
? ChoreographerHasErrorButton(
|
||||
controller.pangeaController,
|
||||
controller.choreographer.errorService.error!,
|
||||
)
|
||||
: controller.showPermissionsError
|
||||
? LanguagePermissionsButtons(
|
||||
choreographer: controller.choreographer,
|
||||
roomID: controller.roomId,
|
||||
)
|
||||
: null)
|
||||
// #Pangea
|
||||
: null,
|
||||
// ? Padding(
|
||||
// padding: const EdgeInsets.only(bottom: 56.0),
|
||||
// child: FloatingActionButton(
|
||||
// onPressed: controller.scrollDown,
|
||||
// heroTag: null,
|
||||
// mini: true,
|
||||
// child: const Icon(Icons.arrow_downward_outlined),
|
||||
// ),
|
||||
// )
|
||||
// : null,
|
||||
floatingActionButton: ChatFloatingActionButton(
|
||||
controller: controller,
|
||||
),
|
||||
// Pangea#
|
||||
body:
|
||||
// #Pangea
|
||||
// DropTarget(
|
||||
|
|
@ -338,120 +324,100 @@ class ChatView extends StatelessWidget {
|
|||
),
|
||||
if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join)
|
||||
// #Pangea
|
||||
// Container(
|
||||
ConditionalFlexible(
|
||||
isScroll: controller.isRowScrollable,
|
||||
child: ConditionalScroll(
|
||||
isScroll: controller.isRowScrollable,
|
||||
child: MeasurableWidget(
|
||||
onChange: (size, position) {
|
||||
controller.inputRowSize = size!.height;
|
||||
},
|
||||
child: Container(
|
||||
// Pangea#
|
||||
margin: EdgeInsets.only(
|
||||
bottom: bottomSheetPadding,
|
||||
left: bottomSheetPadding,
|
||||
right: bottomSheetPadding,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
// ignore: deprecated_member_use
|
||||
.surfaceVariant,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
child: controller.room.isAbandonedDMRoom ==
|
||||
true
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// #Pangea
|
||||
if (controller.room.isRoomAdmin)
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
foregroundColor:
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.archiveChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.archive,
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
foregroundColor:
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
// #Pangea
|
||||
// Icons.archive_outlined,
|
||||
Icons.arrow_forward,
|
||||
// Pangea#
|
||||
),
|
||||
onPressed: controller.leaveChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.leave,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.forum_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.reopenChat,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ITBar(
|
||||
choreographer:
|
||||
controller.choreographer,
|
||||
),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: bottomSheetPadding,
|
||||
left: bottomSheetPadding,
|
||||
right: bottomSheetPadding,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
// ignore: deprecated_member_use
|
||||
.surfaceVariant,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
child: controller.room.isAbandonedDMRoom == true
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// #Pangea
|
||||
if (controller.room.isRoomAdmin)
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
onPressed: controller.archiveChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.archive,
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
// #Pangea
|
||||
// Icons.archive_outlined,
|
||||
Icons.arrow_forward,
|
||||
// Pangea#
|
||||
),
|
||||
onPressed: controller.leaveChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.leave,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.forum_outlined,
|
||||
),
|
||||
onPressed: controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.reopenChat,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ITBar(
|
||||
choreographer:
|
||||
controller.choreographer,
|
||||
),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -484,35 +450,3 @@ class ChatView extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
Widget ConditionalFlexible({required bool isScroll, required Widget child}) {
|
||||
if (isScroll) {
|
||||
return Flexible(
|
||||
flex: 9999999,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
class ConditionalScroll extends StatelessWidget {
|
||||
final bool isScroll;
|
||||
final Widget child;
|
||||
const ConditionalScroll({
|
||||
super.key,
|
||||
required this.isScroll,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isScroll) {
|
||||
return SingleChildScrollView(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/enum/use_type.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
|
|
@ -39,7 +38,6 @@ class Message extends StatelessWidget {
|
|||
final bool animateIn;
|
||||
final void Function()? resetAnimateIn;
|
||||
// #Pangea
|
||||
final LanguageModel? selectedDisplayLang;
|
||||
final bool immersionMode;
|
||||
final bool definitions;
|
||||
final ChatController controller;
|
||||
|
|
@ -64,7 +62,6 @@ class Message extends StatelessWidget {
|
|||
this.resetAnimateIn,
|
||||
this.avatarPresenceBackgroundColor,
|
||||
// #Pangea
|
||||
required this.selectedDisplayLang,
|
||||
required this.immersionMode,
|
||||
required this.definitions,
|
||||
required this.controller,
|
||||
|
|
@ -82,9 +79,9 @@ class Message extends StatelessWidget {
|
|||
// #Pangea
|
||||
debugPrint('Message.build()');
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (controller.edittingEvents.contains(event.eventId)) {
|
||||
if (controller.pangeaEditingEvent?.eventId == event.eventId) {
|
||||
pangeaMessageEvent?.updateLatestEdit();
|
||||
controller.clearEdittingEvent(event.eventId);
|
||||
controller.clearEditingEvent();
|
||||
}
|
||||
});
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_det
|
|||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/utils/lock_room.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
|
||||
|
|
@ -269,11 +268,14 @@ class ChatDetailsView extends StatelessWidget {
|
|||
// roomId: controller.roomId,
|
||||
// startOpen: false,
|
||||
// ),
|
||||
if (room.pangeaRoomRules != null)
|
||||
RoomRulesEditor(
|
||||
roomId: controller.roomId,
|
||||
startOpen: false,
|
||||
),
|
||||
|
||||
// Commenting out pangea room rules for now
|
||||
// if (room.pangeaRoomRules != null)
|
||||
// RoomRulesEditor(
|
||||
// roomId: controller.roomId,
|
||||
// startOpen: false,
|
||||
// ),
|
||||
|
||||
// if (!room.canChangeStateEvent(EventTypes.RoomTopic))
|
||||
// ListTile(
|
||||
// title: Text(
|
||||
|
|
@ -434,7 +436,9 @@ class ChatDetailsView extends StatelessWidget {
|
|||
onTap: () =>
|
||||
context.go('/rooms/${room.id}/invite'),
|
||||
),
|
||||
if (room.showClassEditOptions && room.isSpace)
|
||||
if (room.showClassEditOptions &&
|
||||
room.isSpace &&
|
||||
!room.isSubspace)
|
||||
SpaceDetailsToggleAddStudentsTile(
|
||||
controller: controller,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -476,6 +476,8 @@ class ChatListController extends State<ChatList>
|
|||
StreamSubscription? classStream;
|
||||
StreamSubscription? _invitedSpaceSubscription;
|
||||
StreamSubscription? _subscriptionStatusStream;
|
||||
StreamSubscription? _spaceChildSubscription;
|
||||
final Set<String> hasUpdates = {};
|
||||
//Pangea#
|
||||
|
||||
@override
|
||||
|
|
@ -567,6 +569,16 @@ class ChatListController extends State<ChatList>
|
|||
showSubscribedSnackbar(context);
|
||||
}
|
||||
});
|
||||
|
||||
// listen for space child updates for any space that is not the active space
|
||||
// so that when the user navigates to the space that was updated, it will
|
||||
// reload any rooms that have been added / removed
|
||||
final client = pangeaController.matrixState.client;
|
||||
_spaceChildSubscription ??= client.onRoomState.stream.where((u) {
|
||||
return u.state.type == EventTypes.SpaceChild && u.roomId != activeSpaceId;
|
||||
}).listen((update) {
|
||||
hasUpdates.add(update.roomId);
|
||||
});
|
||||
//Pangea#
|
||||
|
||||
super.initState();
|
||||
|
|
@ -581,6 +593,7 @@ class ChatListController extends State<ChatList>
|
|||
classStream?.cancel();
|
||||
_invitedSpaceSubscription?.cancel();
|
||||
_subscriptionStatusStream?.cancel();
|
||||
_spaceChildSubscription?.cancel();
|
||||
//Pangea#
|
||||
scrollController.removeListener(_onScroll);
|
||||
super.dispose();
|
||||
|
|
@ -908,13 +921,14 @@ class ChatListController extends State<ChatList>
|
|||
|
||||
// #Pangea
|
||||
if (mounted) {
|
||||
// TODO try not to await so much
|
||||
GoogleAnalytics.analyticsUserUpdate(client.userID);
|
||||
await pangeaController.subscriptionController.initialize();
|
||||
await pangeaController.myAnalytics.initialize();
|
||||
pangeaController.afterSyncAndFirstLoginInitialization(context);
|
||||
await pangeaController.inviteBotToExistingSpaces();
|
||||
await pangeaController.setPangeaPushRules();
|
||||
await client.migrateAnalyticsRooms();
|
||||
client.migrateAnalyticsRooms();
|
||||
} else {
|
||||
ErrorHandler.logError(
|
||||
m: "didn't run afterSyncAndFirstLoginInitialization because not mounted",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
|
|||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/sync_update_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -45,8 +45,8 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
Object? error;
|
||||
bool loading = false;
|
||||
// #Pangea
|
||||
StreamSubscription<SyncUpdate>? _roomSubscription;
|
||||
bool refreshing = false;
|
||||
StreamSubscription? _roomSubscription;
|
||||
|
||||
final String _chatCountsKey = 'chatCounts';
|
||||
Map<String, int> get chatCounts => Map.from(
|
||||
|
|
@ -57,9 +57,33 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
loadHierarchy();
|
||||
// #Pangea
|
||||
// loadHierarchy();
|
||||
|
||||
// If, on launch, this room has had updates to its children,
|
||||
// ensure the hierarchy is properly reloaded
|
||||
final bool hasUpdate = widget.controller.hasUpdates.contains(
|
||||
widget.controller.activeSpaceId,
|
||||
);
|
||||
|
||||
loadHierarchy(hasUpdate: hasUpdate).then(
|
||||
// remove this space ID from the set of space IDs with updates
|
||||
(_) => widget.controller.hasUpdates.remove(
|
||||
widget.controller.activeSpaceId,
|
||||
),
|
||||
);
|
||||
|
||||
loadChatCounts();
|
||||
|
||||
// Listen for changes to the activeSpace's hierarchy,
|
||||
// and reload the hierarchy when they come through
|
||||
final client = Matrix.of(context).client;
|
||||
_roomSubscription ??= client.onRoomState.stream.where((u) {
|
||||
return u.state.type == EventTypes.SpaceChild &&
|
||||
u.roomId == widget.controller.activeSpaceId;
|
||||
}).listen((update) {
|
||||
loadHierarchy(hasUpdate: true);
|
||||
});
|
||||
// Pangea#
|
||||
super.initState();
|
||||
}
|
||||
|
|
@ -75,90 +99,184 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
void _refresh() {
|
||||
// #Pangea
|
||||
// _lastResponse.remove(widget.controller.activseSpaceId);
|
||||
if (mounted) {
|
||||
// Pangea#
|
||||
loadHierarchy();
|
||||
// #Pangea
|
||||
}
|
||||
// loadHierarchy();
|
||||
if (mounted) setState(() => refreshing = true);
|
||||
loadHierarchy(hasUpdate: true).whenComplete(() {
|
||||
if (mounted) setState(() => refreshing = false);
|
||||
});
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
Future<GetSpaceHierarchyResponse> loadHierarchy([
|
||||
String? prevBatch,
|
||||
// #Pangea
|
||||
// #Pangea
|
||||
// Future<GetSpaceHierarchyResponse?> loadHierarchy([String? prevBatch]) async {
|
||||
// final activeSpaceId = widget.controller.activeSpaceId;
|
||||
// if (activeSpaceId == null) return null;
|
||||
// final client = Matrix.of(context).client;
|
||||
|
||||
// final activeSpace = client.getRoomById(activeSpaceId);
|
||||
// await activeSpace?.postLoad();
|
||||
|
||||
// setState(() {
|
||||
// error = null;
|
||||
// loading = true;
|
||||
// });
|
||||
|
||||
// try {
|
||||
// final response = await client.getSpaceHierarchy(
|
||||
// activeSpaceId,
|
||||
// maxDepth: 1,
|
||||
// from: prevBatch,
|
||||
// );
|
||||
|
||||
// if (prevBatch != null) {
|
||||
// response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []);
|
||||
// }
|
||||
// setState(() {
|
||||
// _lastResponse[activeSpaceId] = response;
|
||||
// });
|
||||
// return _lastResponse[activeSpaceId]!;
|
||||
// } catch (e) {
|
||||
// setState(() {
|
||||
// error = e;
|
||||
// });
|
||||
// rethrow;
|
||||
// } finally {
|
||||
// setState(() {
|
||||
// loading = false;
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Loads the hierarchy of the active space (or the given spaceId) and stores
|
||||
/// it in _lastResponse map. If there's already a response in that map for the
|
||||
/// spaceId, it will try to load the next batch and add the new rooms to the
|
||||
/// already loaded ones. Displays a loading indicator while loading, and an error
|
||||
/// message if an error occurs.
|
||||
/// If hasUpdate is true, it will force the hierarchy to be reloaded.
|
||||
Future<void> loadHierarchy({
|
||||
String? spaceId,
|
||||
// Pangea#
|
||||
]) async {
|
||||
// #Pangea
|
||||
bool hasUpdate = false,
|
||||
}) async {
|
||||
if ((widget.controller.activeSpaceId == null && spaceId == null) ||
|
||||
loading) {
|
||||
return GetSpaceHierarchyResponse(
|
||||
rooms: [],
|
||||
nextBatch: null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
error = null;
|
||||
loading = true;
|
||||
});
|
||||
// Pangea#
|
||||
|
||||
// #Pangea
|
||||
// final activeSpaceId = widget.controller.activeSpaceId!;
|
||||
final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!;
|
||||
// Pangea#
|
||||
final client = Matrix.of(context).client;
|
||||
|
||||
final activeSpace = client.getRoomById(activeSpaceId);
|
||||
await activeSpace?.postLoad();
|
||||
|
||||
// #Pangea
|
||||
// setState(() {
|
||||
// error = null;
|
||||
// loading = true;
|
||||
// });
|
||||
// Pangea#
|
||||
loading = true;
|
||||
error = null;
|
||||
setState(() {});
|
||||
|
||||
try {
|
||||
await _loadHierarchy(spaceId: spaceId, hasUpdate: hasUpdate);
|
||||
} catch (e, s) {
|
||||
if (mounted) {
|
||||
setState(() => error = e);
|
||||
}
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal logic of loadHierarchy. It will load the hierarchy of
|
||||
/// the active space id (or specified spaceId).
|
||||
Future<void> _loadHierarchy({
|
||||
String? spaceId,
|
||||
bool hasUpdate = false,
|
||||
}) async {
|
||||
final client = Matrix.of(context).client;
|
||||
final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!;
|
||||
final activeSpace = client.getRoomById(activeSpaceId);
|
||||
|
||||
if (activeSpace == null) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception('Space not found in loadHierarchy'),
|
||||
data: {'spaceId': activeSpaceId},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load all of the space's state events. Space Child events
|
||||
// are used to filtering out unsuggested, unjoined rooms.
|
||||
await activeSpace.postLoad();
|
||||
|
||||
// The current number of rooms loaded for this space that are visible in the UI
|
||||
final int prevLength = _lastResponse[activeSpaceId] != null && !hasUpdate
|
||||
? filterHierarchyResponse(
|
||||
activeSpace,
|
||||
_lastResponse[activeSpaceId]!.rooms,
|
||||
).length
|
||||
: 0;
|
||||
|
||||
// Failsafe to prevent too many calls to the server in a row
|
||||
int callsToServer = 0;
|
||||
|
||||
GetSpaceHierarchyResponse? currentHierarchy =
|
||||
hasUpdate ? null : _lastResponse[activeSpaceId];
|
||||
|
||||
// Makes repeated calls to the server until 10 new visible rooms have
|
||||
// been loaded, or there are no rooms left to load. Using a loop here,
|
||||
// rather than one single call to the endpoint, because some spaces have
|
||||
// so many invisible rooms (analytics rooms) that it might look like
|
||||
// pressing the 'load more' button does nothing (Because the only rooms
|
||||
// coming through from those calls are analytics rooms).
|
||||
while (callsToServer < 5) {
|
||||
// if this space has been loaded and there are no more rooms to load, break
|
||||
if (currentHierarchy != null && currentHierarchy.nextBatch == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
// if this space has been loaded and 10 new rooms have been loaded, break
|
||||
if (currentHierarchy != null) {
|
||||
final int currentLength = filterHierarchyResponse(
|
||||
activeSpace,
|
||||
currentHierarchy.rooms,
|
||||
).length;
|
||||
|
||||
if (currentLength - prevLength >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// make the call to the server
|
||||
final response = await client.getSpaceHierarchy(
|
||||
activeSpaceId,
|
||||
maxDepth: 1,
|
||||
from: prevBatch,
|
||||
// #Pangea
|
||||
from: currentHierarchy?.nextBatch,
|
||||
limit: 100,
|
||||
// Pangea#
|
||||
);
|
||||
callsToServer++;
|
||||
|
||||
if (prevBatch != null) {
|
||||
response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []);
|
||||
}
|
||||
setState(() {
|
||||
_lastResponse[activeSpaceId] = response;
|
||||
});
|
||||
return _lastResponse[activeSpaceId]!;
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e;
|
||||
});
|
||||
rethrow;
|
||||
} finally {
|
||||
// #Pangea
|
||||
if (activeSpace != null) {
|
||||
await setChatCount(
|
||||
activeSpace,
|
||||
_lastResponse[activeSpaceId] ??
|
||||
GetSpaceHierarchyResponse(
|
||||
rooms: [],
|
||||
),
|
||||
// if rooms have earlier been loaded for this space, add those
|
||||
// previously loaded rooms to the front of the response list
|
||||
if (currentHierarchy != null) {
|
||||
response.rooms.insertAll(
|
||||
0,
|
||||
currentHierarchy.rooms,
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
|
||||
// finally, set the response to the last response for this space
|
||||
currentHierarchy = response;
|
||||
}
|
||||
|
||||
if (currentHierarchy != null) {
|
||||
_lastResponse[activeSpaceId] = currentHierarchy;
|
||||
}
|
||||
|
||||
// After making those calls to the server, set the chat count for
|
||||
// this space. Used for the UI of the 'All Spaces' view
|
||||
setChatCount(
|
||||
activeSpace,
|
||||
_lastResponse[activeSpaceId] ??
|
||||
GetSpaceHierarchyResponse(
|
||||
rooms: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
void _onJoinSpaceChild(SpaceRoomsChunk spaceChild) async {
|
||||
final client = Matrix.of(context).client;
|
||||
|
|
@ -454,71 +572,60 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
|
||||
// #Pangea
|
||||
Future<void> loadChatCounts() async {
|
||||
for (final Room room in Matrix.of(context).client.rooms) {
|
||||
if (room.isSpace && !chatCounts.containsKey(room.id)) {
|
||||
await loadHierarchy(null, room.id);
|
||||
// if not in the call spaces view, don't load chat count yet
|
||||
if (widget.controller.activeSpaceId != null) return;
|
||||
|
||||
final List<Room> allSpaces =
|
||||
Matrix.of(context).client.rooms.where((room) => room.isSpace).toList();
|
||||
|
||||
for (final Room space in allSpaces) {
|
||||
// check if the space is visible in the all spaces list
|
||||
final bool isRootSpace = !allSpaces.any(
|
||||
(parentSpace) =>
|
||||
parentSpace.spaceChildren.any((child) => child.roomId == space.id),
|
||||
);
|
||||
|
||||
// if it's visible, and it hasn't been loaded yet, load chat count
|
||||
if (isRootSpace && !chatCounts.containsKey(space.id)) {
|
||||
loadHierarchy(spaceId: space.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshOnUpdate(SyncUpdate event) async {
|
||||
/* refresh on leave, invite, and space child update
|
||||
not join events, because there's already a listener on
|
||||
onTapSpaceChild, and they interfere with each other */
|
||||
if (widget.controller.activeSpaceId == null || !mounted || refreshing) {
|
||||
return;
|
||||
}
|
||||
setState(() => refreshing = true);
|
||||
final client = Matrix.of(context).client;
|
||||
if (mounted &&
|
||||
event.isMembershipUpdateByType(
|
||||
Membership.leave,
|
||||
client.userID!,
|
||||
) ||
|
||||
event.isMembershipUpdateByType(
|
||||
Membership.invite,
|
||||
client.userID!,
|
||||
) ||
|
||||
event.isSpaceChildUpdate(
|
||||
widget.controller.activeSpaceId!,
|
||||
)) {
|
||||
await loadHierarchy();
|
||||
}
|
||||
setState(() => refreshing = false);
|
||||
}
|
||||
bool includeSpaceChild(
|
||||
Room space,
|
||||
SpaceRoomsChunk hierarchyMember,
|
||||
) {
|
||||
if (!mounted) return false;
|
||||
final bool isAnalyticsRoom =
|
||||
hierarchyMember.roomType == PangeaRoomTypes.analytics;
|
||||
|
||||
bool includeSpaceChild(sc, matchingSpaceChildren) {
|
||||
final bool isAnalyticsRoom = sc.roomType == PangeaRoomTypes.analytics;
|
||||
final bool isMember = [Membership.join, Membership.invite]
|
||||
.contains(Matrix.of(context).client.getRoomById(sc.roomId)?.membership);
|
||||
final bool isSuggested = matchingSpaceChildren.any(
|
||||
(matchingSpaceChild) =>
|
||||
matchingSpaceChild.roomId == sc.roomId &&
|
||||
matchingSpaceChild.suggested == true,
|
||||
final bool isMember = [Membership.join, Membership.invite].contains(
|
||||
Matrix.of(context).client.getRoomById(hierarchyMember.roomId)?.membership,
|
||||
);
|
||||
|
||||
final bool isSuggested =
|
||||
space.spaceChildSuggestionStatus[hierarchyMember.roomId] ?? true;
|
||||
|
||||
return !isAnalyticsRoom && (isMember || isSuggested);
|
||||
}
|
||||
|
||||
List<SpaceRoomsChunk> filterSpaceChildren(
|
||||
List<SpaceRoomsChunk> filterHierarchyResponse(
|
||||
Room space,
|
||||
List<SpaceRoomsChunk> spaceChildren,
|
||||
List<SpaceRoomsChunk> hierarchyResponse,
|
||||
) {
|
||||
final childIds =
|
||||
spaceChildren.map((hierarchyMember) => hierarchyMember.roomId);
|
||||
final List<SpaceRoomsChunk> filteredChildren = [];
|
||||
for (final child in hierarchyResponse) {
|
||||
final isDuplicate = filteredChildren.any(
|
||||
(filtered) => filtered.roomId == child.roomId,
|
||||
);
|
||||
if (isDuplicate) continue;
|
||||
|
||||
final matchingSpaceChildren = space.spaceChildren
|
||||
.where((spaceChild) => childIds.contains(spaceChild.roomId))
|
||||
.toList();
|
||||
|
||||
final filteredSpaceChildren = spaceChildren
|
||||
.where(
|
||||
(sc) => includeSpaceChild(
|
||||
sc,
|
||||
matchingSpaceChildren,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return filteredSpaceChildren;
|
||||
if (includeSpaceChild(space, child)) {
|
||||
filteredChildren.add(child);
|
||||
}
|
||||
}
|
||||
return filteredChildren;
|
||||
}
|
||||
|
||||
int sortSpaceChildren(
|
||||
|
|
@ -542,7 +649,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
) async {
|
||||
final Map<String, int> updatedChatCounts = Map.from(chatCounts);
|
||||
final List<SpaceRoomsChunk> spaceChildren = response?.rooms ?? [];
|
||||
final filteredChildren = filterSpaceChildren(space, spaceChildren)
|
||||
final filteredChildren = filterHierarchyResponse(space, spaceChildren)
|
||||
.where((sc) => sc.roomId != space.id)
|
||||
.toList();
|
||||
updatedChatCounts[space.id] = filteredChildren.length;
|
||||
|
|
@ -666,12 +773,6 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
);
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
_roomSubscription ??= client.onSync.stream
|
||||
.where((event) => event.hasRoomUpdate)
|
||||
.listen(refreshOnUpdate);
|
||||
// Pangea#
|
||||
|
||||
final parentSpace = allSpaces.firstWhereOrNull(
|
||||
(space) =>
|
||||
space.spaceChildren.any((child) => child.roomId == activeSpaceId),
|
||||
|
|
@ -774,7 +875,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
final space =
|
||||
Matrix.of(context).client.getRoomById(activeSpaceId);
|
||||
if (space != null) {
|
||||
spaceChildren = filterSpaceChildren(space, spaceChildren);
|
||||
spaceChildren = filterHierarchyResponse(space, spaceChildren);
|
||||
}
|
||||
spaceChildren.sort(sortSpaceChildren);
|
||||
// Pangea#
|
||||
|
|
@ -793,7 +894,10 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
onPressed: loading
|
||||
? null
|
||||
: () {
|
||||
loadHierarchy(response.nextBatch);
|
||||
// #Pangea
|
||||
// loadHierarchy(response.nextBatch);
|
||||
loadHierarchy();
|
||||
// Pangea#
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ class LoginView extends StatelessWidget {
|
|||
controller.showPassword
|
||||
? Icons.visibility_off_outlined
|
||||
: Icons.visibility_outlined,
|
||||
color: Colors.black,
|
||||
// color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluffychat/pages/new_space/new_space_view.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
|
|
@ -32,7 +29,7 @@ class NewSpaceController extends State<NewSpace> {
|
|||
// #Pangea
|
||||
// bool publicGroup = false;
|
||||
bool publicGroup = true;
|
||||
final GlobalKey<RoomRulesState> rulesEditorKey = GlobalKey<RoomRulesState>();
|
||||
// final GlobalKey<RoomRulesState> rulesEditorKey = GlobalKey<RoomRulesState>();
|
||||
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
|
||||
// commenting out language settings in spaces for now
|
||||
// final GlobalKey<LanguageSettingsState> languageSettingsKey =
|
||||
|
|
@ -87,11 +84,12 @@ class NewSpaceController extends State<NewSpace> {
|
|||
),
|
||||
);
|
||||
|
||||
if (rulesEditorKey.currentState?.rules != null) {
|
||||
events.add(rulesEditorKey.currentState!.rules.toStateEvent);
|
||||
} else {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
// commenting out pangea room rules in spaces for now
|
||||
// if (rulesEditorKey.currentState?.rules != null) {
|
||||
// events.add(rulesEditorKey.currentState!.rules.toStateEvent);
|
||||
// } else {
|
||||
// debugger(when: kDebugMode);
|
||||
// }
|
||||
// commenting out language settings in spaces for now
|
||||
// if (languageSettingsKey.currentState != null) {
|
||||
// events
|
||||
|
|
@ -110,10 +108,11 @@ class NewSpaceController extends State<NewSpace> {
|
|||
// Pangea#
|
||||
});
|
||||
// #Pangea
|
||||
if (rulesEditorKey.currentState == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
// commenting out pangea room rules in spaces for now
|
||||
// if (rulesEditorKey.currentState == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return;
|
||||
// }
|
||||
// commenting out language settings in spaces for now
|
||||
// if (languageSettingsKey.currentState != null &&
|
||||
// languageSettingsKey.currentState!.sameLanguages) {
|
||||
|
|
@ -174,15 +173,17 @@ class NewSpaceController extends State<NewSpace> {
|
|||
addToSpaceKey.currentState!.parent,
|
||||
)
|
||||
: null,
|
||||
// initialState: [
|
||||
// if (avatar != null)
|
||||
// sdk.StateEvent(
|
||||
// type: sdk.EventTypes.RoomAvatar,
|
||||
// content: {'url': avatarUrl.toString()},
|
||||
// ),
|
||||
// ],
|
||||
initialState: initialState,
|
||||
// Pangea#
|
||||
initialState: [
|
||||
if (avatar != null)
|
||||
sdk.StateEvent(
|
||||
type: sdk.EventTypes.RoomAvatar,
|
||||
content: {'url': avatarUrl.toString()},
|
||||
),
|
||||
// #Pangea
|
||||
...initialState,
|
||||
// Pangea#
|
||||
],
|
||||
);
|
||||
// #Pangea
|
||||
final List<Future<dynamic>> futures = [
|
||||
|
|
@ -198,14 +199,6 @@ class NewSpaceController extends State<NewSpace> {
|
|||
if (capacity != null && space != null) {
|
||||
space.updateRoomCapacity(capacity);
|
||||
}
|
||||
final newChatRoomId = await Matrix.of(context).client.createGroupChat(
|
||||
enableEncryption: false,
|
||||
preset: sdk.CreateRoomPreset.publicChat,
|
||||
// Welcome chat name is '[space name acronym]: Welcome Chat'
|
||||
groupName:
|
||||
'${nameController.text.trim().split(RegExp(r"\s+")).map((s) => s[0]).join()}: ${L10n.of(context)!.classWelcomeChat}',
|
||||
);
|
||||
GoogleAnalytics.createChat(newChatRoomId);
|
||||
|
||||
final Room? room = Matrix.of(context).client.getRoomById(spaceId);
|
||||
if (room == null) {
|
||||
|
|
@ -217,12 +210,6 @@ class NewSpaceController extends State<NewSpace> {
|
|||
return;
|
||||
}
|
||||
|
||||
room.setSpaceChild(newChatRoomId, suggested: true);
|
||||
GoogleAnalytics.addParent(
|
||||
newChatRoomId,
|
||||
room.classCode,
|
||||
);
|
||||
|
||||
GoogleAnalytics.createClass(room.name, room.classCode);
|
||||
try {
|
||||
await room.invite(BotName.byEnvironment);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
|
|
@ -133,35 +129,39 @@ class NewSpaceView extends StatelessWidget {
|
|||
startOpen: true,
|
||||
spaceMode: true,
|
||||
),
|
||||
if (controller.rulesEditorKey.currentState != null)
|
||||
RoomRulesEditor(
|
||||
key: controller.rulesEditorKey,
|
||||
roomId: null,
|
||||
startOpen: false,
|
||||
initialRules: controller.rulesEditorKey.currentState!.rules,
|
||||
),
|
||||
if (controller.rulesEditorKey.currentState == null)
|
||||
FutureBuilder<PangeaRoomRules?>(
|
||||
future: Matrix.of(context).client.lastUpdatedRoomRules,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return RoomRulesEditor(
|
||||
key: controller.rulesEditorKey,
|
||||
roomId: null,
|
||||
startOpen: false,
|
||||
initialRules: snapshot.data,
|
||||
);
|
||||
} else {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child:
|
||||
CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
// Commenting out pangea room rules for now
|
||||
// if (controller.rulesEditorKey.currentState != null)
|
||||
// RoomRulesEditor(
|
||||
// key: controller.rulesEditorKey,
|
||||
// roomId: null,
|
||||
// startOpen: false,
|
||||
// initialRules: controller.rulesEditorKey.currentState!.rules,
|
||||
// ),
|
||||
|
||||
// Commenting out pangea room rules for now
|
||||
// if (controller.rulesEditorKey.currentState == null)
|
||||
// FutureBuilder<PangeaRoomRules?>(
|
||||
// future: Matrix.of(context).client.lastUpdatedRoomRules,
|
||||
// builder: (context, snapshot) {
|
||||
// if (snapshot.connectionState == ConnectionState.done) {
|
||||
// return RoomRulesEditor(
|
||||
// key: controller.rulesEditorKey,
|
||||
// roomId: null,
|
||||
// startOpen: false,
|
||||
// initialRules: snapshot.data,
|
||||
// );
|
||||
// } else {
|
||||
// return const Padding(
|
||||
// padding: EdgeInsets.all(16.0),
|
||||
// child: Center(
|
||||
// child:
|
||||
// CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
|
||||
// SwitchListTile.adaptive(
|
||||
// title: Text(L10n.of(context)!.spaceIsPublic),
|
||||
// value: controller.publicGroup,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'dart:developer';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
||||
|
|
@ -38,7 +37,6 @@ class Choreographer {
|
|||
late PangeaTextController _textController;
|
||||
late ITController itController;
|
||||
late IgcController igc;
|
||||
late MessageOptions messageOptions;
|
||||
late AlternativeTranslator altTranslator;
|
||||
late ErrorService errorService;
|
||||
|
||||
|
|
@ -59,7 +57,6 @@ class Choreographer {
|
|||
_textController = PangeaTextController(choreographer: this);
|
||||
itController = ITController(this);
|
||||
igc = IgcController(this);
|
||||
messageOptions = MessageOptions(this);
|
||||
errorService = ErrorService(this);
|
||||
altTranslator = AlternativeTranslator(this);
|
||||
_textController.addListener(_onChangeListener);
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
|
||||
class MessageOptions {
|
||||
Choreographer choreographer;
|
||||
LanguageModel? _selectedDisplayLang;
|
||||
|
||||
MessageOptions(this.choreographer);
|
||||
|
||||
LanguageModel? get selectedDisplayLang {
|
||||
if (_selectedDisplayLang != null &&
|
||||
_selectedDisplayLang!.langCode != LanguageKeys.unknownLanguage) {
|
||||
return _selectedDisplayLang;
|
||||
}
|
||||
_selectedDisplayLang = choreographer.l2Lang;
|
||||
return _selectedDisplayLang;
|
||||
}
|
||||
|
||||
bool get isTranslationOn =>
|
||||
_selectedDisplayLang?.langCode != choreographer.l2LangCode;
|
||||
|
||||
// void setSelectedDisplayLang(LanguageModel? newLang) {
|
||||
// _selectedDisplayLang = newLang;
|
||||
// choreographer.setState();
|
||||
// }
|
||||
|
||||
void toggleSelectedDisplayLang() {
|
||||
if (_selectedDisplayLang?.langCode == choreographer.l2LangCode) {
|
||||
_selectedDisplayLang = choreographer.l1Lang;
|
||||
} else {
|
||||
_selectedDisplayLang = choreographer.l2Lang;
|
||||
}
|
||||
debugPrint('toggleSelectedDisplayLang: ${_selectedDisplayLang?.langCode}');
|
||||
choreographer.setState();
|
||||
GoogleAnalytics.messageTranslate();
|
||||
}
|
||||
|
||||
void resetSelectedDisplayLang() {
|
||||
_selectedDisplayLang = choreographer.l2Lang;
|
||||
choreographer.setState();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
|
|
@ -7,7 +8,6 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -18,112 +18,140 @@ import '../../utils/overlay.dart';
|
|||
import '../../widgets/igc/word_data_card.dart';
|
||||
import 'choice_array.dart';
|
||||
|
||||
class ITBar extends StatelessWidget {
|
||||
class ITBar extends StatefulWidget {
|
||||
final Choreographer choreographer;
|
||||
const ITBar({super.key, required this.choreographer});
|
||||
|
||||
ITController get itController => choreographer.itController;
|
||||
@override
|
||||
ITBarState createState() => ITBarState();
|
||||
}
|
||||
|
||||
class ITBarState extends State<ITBar> {
|
||||
ITController get itController => widget.choreographer.itController;
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Rebuild the widget each time there's an update from choreo.
|
||||
_choreoSub = widget.choreographer.stateListener.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return AnimatedSize(
|
||||
duration: itController.willOpen
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
clipBehavior: Clip.none,
|
||||
child: !itController.willOpen
|
||||
? const SizedBox()
|
||||
: CompositedTransformTarget(
|
||||
link: choreographer.itBarLinkAndKey.link,
|
||||
child: AnimatedOpacity(
|
||||
duration: itController.willOpen
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
opacity: itController.willOpen ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
key: choreographer.itBarLinkAndKey.key,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppConfig.borderRadius),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// // Row(
|
||||
// // mainAxisAlignment: MainAxisAlignment.start,
|
||||
// // crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// // children: [
|
||||
// // CounterDisplay(
|
||||
// // correct: controller.correctChoices,
|
||||
// // custom: controller.customChoices,
|
||||
// // incorrect: controller.incorrectChoices,
|
||||
// // yellow: controller.wildcardChoices,
|
||||
// // ),
|
||||
// // CompositedTransformTarget(
|
||||
// // link: choreographer.itBotLayerLinkAndKey.link,
|
||||
// // child: ITBotButton(
|
||||
// // key: choreographer.itBotLayerLinkAndKey.key,
|
||||
// // choreographer: choreographer,
|
||||
// // ),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// ITCloseButton(choreographer: choreographer),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 40.0),
|
||||
OriginalText(controller: itController),
|
||||
const SizedBox(height: 7.0),
|
||||
IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 80),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Center(
|
||||
child: itController.choreographer.errorService.isError
|
||||
? ITError(
|
||||
error: itController
|
||||
.choreographer.errorService.error!,
|
||||
controller: itController,
|
||||
)
|
||||
: itController.showChoiceFeedback
|
||||
? ChoiceFeedbackText(controller: itController)
|
||||
: itController.isTranslationDone
|
||||
? TranslationFeedback(
|
||||
controller: itController,
|
||||
)
|
||||
: ITChoices(controller: itController),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
? const SizedBox()
|
||||
: CompositedTransformTarget(
|
||||
link: widget.choreographer.itBarLinkAndKey.link,
|
||||
child: AnimatedOpacity(
|
||||
duration: itController.willOpen
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
opacity: itController.willOpen ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
key: widget.choreographer.itBarLinkAndKey.key,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppConfig.borderRadius),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0.0,
|
||||
right: 0.0,
|
||||
child: ITCloseButton(choreographer: choreographer),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// // Row(
|
||||
// // mainAxisAlignment: MainAxisAlignment.start,
|
||||
// // crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// // children: [
|
||||
// // CounterDisplay(
|
||||
// // correct: controller.correctChoices,
|
||||
// // custom: controller.customChoices,
|
||||
// // incorrect: controller.incorrectChoices,
|
||||
// // yellow: controller.wildcardChoices,
|
||||
// // ),
|
||||
// // CompositedTransformTarget(
|
||||
// // link: choreographer.itBotLayerLinkAndKey.link,
|
||||
// // child: ITBotButton(
|
||||
// // key: choreographer.itBotLayerLinkAndKey.key,
|
||||
// // choreographer: choreographer,
|
||||
// // ),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// ITCloseButton(choreographer: choreographer),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 40.0),
|
||||
OriginalText(controller: itController),
|
||||
const SizedBox(height: 7.0),
|
||||
IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: 80),
|
||||
width: double.infinity,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Center(
|
||||
child: itController
|
||||
.choreographer.errorService.isError
|
||||
? ITError(
|
||||
error: itController.choreographer
|
||||
.errorService.error!,
|
||||
controller: itController,
|
||||
)
|
||||
: itController.showChoiceFeedback
|
||||
? ChoiceFeedbackText(
|
||||
controller: itController,
|
||||
)
|
||||
: itController.isTranslationDone
|
||||
? TranslationFeedback(
|
||||
controller: itController,
|
||||
)
|
||||
: ITChoices(
|
||||
controller: itController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0.0,
|
||||
right: 0.0,
|
||||
child:
|
||||
ITCloseButton(choreographer: widget.choreographer),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -199,20 +227,16 @@ class OriginalText extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (
|
||||
!controller.isEditingSourceText
|
||||
&& controller.sourceText != null
|
||||
)
|
||||
if (!controller.isEditingSourceText && controller.sourceText != null)
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
opacity: controller.nextITStep != null
|
||||
? 1.0
|
||||
: 0.0,
|
||||
opacity: controller.nextITStep != null ? 1.0 : 0.0,
|
||||
child: IconButton(
|
||||
onPressed: () => {
|
||||
if (controller.nextITStep != null) {
|
||||
controller.setIsEditingSourceText(true),
|
||||
},
|
||||
if (controller.nextITStep != null)
|
||||
{
|
||||
controller.setIsEditingSourceText(true),
|
||||
},
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
|
|
@ -309,9 +333,9 @@ class ITChoices extends StatelessWidget {
|
|||
choices: controller.currentITStep!.continuances.map((e) {
|
||||
try {
|
||||
return Choice(
|
||||
text: e.text.trim(),
|
||||
color: e.color,
|
||||
isGold: e.description == "best",
|
||||
text: e.text.trim(),
|
||||
color: e.color,
|
||||
isGold: e.description == "best",
|
||||
);
|
||||
} catch (e) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../../pages/chat/chat.dart';
|
||||
|
||||
class LanguageDisplayToggle extends StatelessWidget {
|
||||
const LanguageDisplayToggle({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final ChatController controller;
|
||||
|
||||
get onPressed =>
|
||||
controller.choreographer.messageOptions.toggleSelectedDisplayLang;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// if (!controller.choreographer.translationEnabled) {
|
||||
// return const SizedBox();
|
||||
// }
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: controller.choreographer.messageOptions.isTranslationOn
|
||||
? AppConfig.primaryColor
|
||||
: null,
|
||||
),
|
||||
child: IconButton(
|
||||
tooltip: L10n.of(context)!.toggleLanguages,
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(Icons.translate_outlined),
|
||||
selectedIcon: const Icon(Icons.translate),
|
||||
isSelected: controller.choreographer.messageOptions.isTranslationOn,
|
||||
),
|
||||
);
|
||||
// return Tooltip(
|
||||
// message: L10n.of(context)!.toggleLanguages,
|
||||
// waitDuration: const Duration(milliseconds: 1000),
|
||||
// child: FloatingActionButton(
|
||||
// onPressed: onPressed,
|
||||
// backgroundColor: Colors.white,
|
||||
// mini: false,
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(200), // <-- Radius
|
||||
// ),
|
||||
// child: LanguageFlag(
|
||||
// flagUrl: controller
|
||||
// .choreographer.messageOptions.displayLang?.languageFlag,
|
||||
// size: 50,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,47 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/assistance_state_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../../pages/chat/chat.dart';
|
||||
|
||||
class ChoreographerSendButton extends StatelessWidget {
|
||||
class ChoreographerSendButton extends StatefulWidget {
|
||||
const ChoreographerSendButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final ChatController controller;
|
||||
|
||||
@override
|
||||
State<ChoreographerSendButton> createState() =>
|
||||
ChoreographerSendButtonState();
|
||||
}
|
||||
|
||||
class ChoreographerSendButtonState extends State<ChoreographerSendButton> {
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Rebuild the widget each time there's an update from
|
||||
// choreo. This keeps the spin up-to-date.
|
||||
_choreoSub =
|
||||
widget.controller.choreographer.stateListener.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// commit for cicd
|
||||
return controller.choreographer.isFetching &&
|
||||
controller.choreographer.isAutoIGCEnabled
|
||||
return widget.controller.choreographer.isFetching &&
|
||||
widget.controller.choreographer.isAutoIGCEnabled
|
||||
? Container(
|
||||
height: 56,
|
||||
width: 56,
|
||||
|
|
@ -28,10 +53,10 @@ class ChoreographerSendButton extends StatelessWidget {
|
|||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.send_outlined),
|
||||
color:
|
||||
controller.choreographer.assistanceState.stateColor(context),
|
||||
color: widget.controller.choreographer.assistanceState
|
||||
.stateColor(context),
|
||||
onPressed: () {
|
||||
controller.choreographer.send(context);
|
||||
widget.controller.choreographer.send(context);
|
||||
},
|
||||
tooltip: L10n.of(context)!.send,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -31,18 +31,16 @@ class ClassController extends BaseController {
|
|||
setState(data: {"activeSpaceId": classId});
|
||||
}
|
||||
|
||||
Future<void> fixClassPowerLevels() async {
|
||||
try {
|
||||
final teacherSpaces =
|
||||
await _pangeaController.matrixState.client.spacesImTeaching;
|
||||
final List<Future<void>> classFixes = List<Room>.from(teacherSpaces)
|
||||
.map((adminSpace) => adminSpace.setClassPowerLevels())
|
||||
.toList();
|
||||
await Future.wait(classFixes);
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
}
|
||||
/// For all the spaces that the user is teaching, set the power levels
|
||||
/// to enable all other users to add child rooms to the space.
|
||||
void fixClassPowerLevels() {
|
||||
Future.wait(
|
||||
_pangeaController.matrixState.client.spacesImTeaching.map(
|
||||
(space) => space.setClassPowerLevels().catchError((err, s) {
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> checkForClassCodeAndSubscription(BuildContext context) async {
|
||||
|
|
@ -131,10 +129,10 @@ class ClassController extends BaseController {
|
|||
_pangeaController.matrixState.client.getRoomById(classChunk.roomId);
|
||||
|
||||
// when possible, add user's analytics room the to space they joined
|
||||
await joinedSpace?.addAnalyticsRoomsToSpace();
|
||||
joinedSpace?.addAnalyticsRoomsToSpace();
|
||||
|
||||
// and invite the space's teachers to the user's analytics rooms
|
||||
await joinedSpace?.inviteSpaceTeachersToAnalyticsRooms();
|
||||
joinedSpace?.inviteSpaceTeachersToAnalyticsRooms();
|
||||
GoogleAnalytics.joinClass(classCode);
|
||||
return;
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -198,9 +198,7 @@ class AnalyticsController extends BaseController {
|
|||
// gets all the summary analytics events for the students
|
||||
// in a space since the current timespace's cut off date
|
||||
|
||||
// ensure that all the space's events are loaded (mainly the for langCode)
|
||||
// and that the participants are loaded
|
||||
await space.postLoad();
|
||||
// ensure that the participants of the space are loaded
|
||||
await space.requestParticipants();
|
||||
|
||||
// TODO switch to using list of futures
|
||||
|
|
@ -439,7 +437,6 @@ class AnalyticsController extends BaseController {
|
|||
timeSpan: currentAnalyticsTimeSpan,
|
||||
);
|
||||
}
|
||||
await space.postLoad();
|
||||
}
|
||||
|
||||
DateTime? lastUpdated;
|
||||
|
|
@ -545,7 +542,6 @@ class AnalyticsController extends BaseController {
|
|||
Future<List<ConstructAnalyticsEvent>> allSpaceMemberConstructs(
|
||||
Room space,
|
||||
) async {
|
||||
await space.postLoad();
|
||||
await space.requestParticipants();
|
||||
final List<ConstructAnalyticsEvent> constructEvents = [];
|
||||
for (final student in space.students) {
|
||||
|
|
@ -788,7 +784,6 @@ class AnalyticsController extends BaseController {
|
|||
);
|
||||
return [];
|
||||
}
|
||||
await space.postLoad();
|
||||
}
|
||||
|
||||
DateTime? lastUpdated;
|
||||
|
|
|
|||
|
|
@ -81,8 +81,7 @@ class PangeaController {
|
|||
BuildContext context,
|
||||
) async {
|
||||
await classController.checkForClassCodeAndSubscription(context);
|
||||
// startChatWithBotIfNotPresent();
|
||||
await classController.fixClassPowerLevels();
|
||||
classController.fixClassPowerLevels();
|
||||
}
|
||||
|
||||
/// Initialize controllers
|
||||
|
|
|
|||
|
|
@ -1,48 +1,40 @@
|
|||
part of "client_extension.dart";
|
||||
|
||||
extension AnalyticsClientExtension on Client {
|
||||
// get analytics room matching targetlanguage
|
||||
// if not present, create it and invite teachers of that language
|
||||
// set description to let people know what the hell it is
|
||||
/// Get the logged in user's analytics room matching
|
||||
/// a given langCode. If not present, create it.
|
||||
Future<Room> _getMyAnalyticsRoom(String langCode) async {
|
||||
await roomsLoading;
|
||||
// ensure room state events (room create,
|
||||
// to check for analytics type) are loaded
|
||||
for (final room in rooms) {
|
||||
if (room.partial) await room.postLoad();
|
||||
}
|
||||
|
||||
final Room? analyticsRoom = analyticsRoomLocal(langCode);
|
||||
|
||||
final Room? analyticsRoom = _analyticsRoomLocal(langCode);
|
||||
if (analyticsRoom != null) return analyticsRoom;
|
||||
|
||||
return _makeAnalyticsRoom(langCode);
|
||||
}
|
||||
|
||||
//note: if langCode is null and user has >1 analyticsRooms then this could
|
||||
//return the wrong one. this is to account for when an exchange might not
|
||||
//be in a class.
|
||||
Room? _analyticsRoomLocal(String? langCode, [String? userIdParam]) {
|
||||
/// Get local analytics room for a given langCode and
|
||||
/// optional userId (if not specified, uses current user).
|
||||
/// If user is invited to the room, joins the room.
|
||||
Room? _analyticsRoomLocal(String langCode, [String? userIdParam]) {
|
||||
final Room? analyticsRoom = rooms.firstWhereOrNull((e) {
|
||||
return e.isAnalyticsRoom &&
|
||||
e.isAnalyticsRoomOfUser(userIdParam ?? userID!) &&
|
||||
(langCode != null ? e.isMadeForLang(langCode) : true);
|
||||
e.isMadeForLang(langCode);
|
||||
});
|
||||
if (analyticsRoom != null &&
|
||||
analyticsRoom.membership == Membership.invite) {
|
||||
debugger(when: kDebugMode);
|
||||
analyticsRoom
|
||||
.join()
|
||||
.onError(
|
||||
analyticsRoom.join().onError(
|
||||
(error, stackTrace) =>
|
||||
ErrorHandler.logError(e: error, s: stackTrace),
|
||||
)
|
||||
.then((value) => analyticsRoom.postLoad());
|
||||
);
|
||||
return analyticsRoom;
|
||||
}
|
||||
return analyticsRoom;
|
||||
}
|
||||
|
||||
/// Creates an analytics room with the specified language code and returns the created room.
|
||||
/// Additionally, the room is added to the user's spaces and all teachers are invited to the room.
|
||||
///
|
||||
/// If the room does not appear immediately after creation, this method waits for it to appear in sync.
|
||||
/// Returns the created [Room] object.
|
||||
Future<Room> _makeAnalyticsRoom(String langCode) async {
|
||||
final String roomID = await createRoom(
|
||||
creationContent: {
|
||||
|
|
@ -53,7 +45,6 @@ extension AnalyticsClientExtension on Client {
|
|||
topic: "This room stores learning analytics for $userID.",
|
||||
invite: [
|
||||
...(await myTeachers).map((e) => e.id),
|
||||
// BotName.localBot,
|
||||
BotName.byEnvironment,
|
||||
],
|
||||
);
|
||||
|
|
@ -66,14 +57,14 @@ extension AnalyticsClientExtension on Client {
|
|||
|
||||
// add this analytics room to all spaces so teachers can join them
|
||||
// via the space hierarchy
|
||||
await analyticsRoom?.addAnalyticsRoomToSpaces();
|
||||
analyticsRoom?.addAnalyticsRoomToSpaces();
|
||||
|
||||
// and invite all teachers to new analytics room
|
||||
await analyticsRoom?.inviteTeachersToAnalyticsRoom();
|
||||
analyticsRoom?.inviteTeachersToAnalyticsRoom();
|
||||
return getRoomById(roomID)!;
|
||||
}
|
||||
|
||||
// Get all my analytics rooms
|
||||
/// Get all my analytics rooms
|
||||
List<Room> get _allMyAnalyticsRooms => rooms
|
||||
.where(
|
||||
(e) => e.isAnalyticsRoomOfUser(userID!),
|
||||
|
|
@ -83,76 +74,77 @@ extension AnalyticsClientExtension on Client {
|
|||
// migration function to change analytics rooms' vsibility to public
|
||||
// so they will appear in the space hierarchy
|
||||
Future<void> _updateAnalyticsRoomVisibility() async {
|
||||
final List<Future> makePublicFutures = [];
|
||||
for (final Room room in allMyAnalyticsRooms) {
|
||||
final visability = await getRoomVisibilityOnDirectory(room.id);
|
||||
if (visability != Visibility.public) {
|
||||
await setRoomVisibilityOnDirectory(
|
||||
room.id,
|
||||
visibility: Visibility.public,
|
||||
);
|
||||
}
|
||||
}
|
||||
await Future.wait(makePublicFutures);
|
||||
await Future.wait(
|
||||
allMyAnalyticsRooms.map((room) async {
|
||||
final visability = await getRoomVisibilityOnDirectory(room.id);
|
||||
if (visability != Visibility.public) {
|
||||
await setRoomVisibilityOnDirectory(
|
||||
room.id,
|
||||
visibility: Visibility.public,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Add all the users' analytics room to all the spaces the student studies in
|
||||
// So teachers can join them via space hierarchy
|
||||
// Will not always work, as there may be spaces where students don't have permission to add chats
|
||||
// But allows teachers to join analytics rooms without being invited
|
||||
Future<void> _addAnalyticsRoomsToAllSpaces() async {
|
||||
final List<Future> addFutures = [];
|
||||
/// Add all the users' analytics room to all the spaces the user is studying in
|
||||
/// so teachers can join them via space hierarchy.
|
||||
/// Allows teachers to join analytics rooms without being invited.
|
||||
void _addAnalyticsRoomsToAllSpaces() {
|
||||
for (final Room room in allMyAnalyticsRooms) {
|
||||
addFutures.add(room.addAnalyticsRoomToSpaces());
|
||||
room.addAnalyticsRoomToSpaces();
|
||||
}
|
||||
await Future.wait(addFutures);
|
||||
}
|
||||
|
||||
// Invite teachers to all my analytics room
|
||||
// Handles case when students cannot add analytics room to space(s)
|
||||
// So teacher is still able to get analytics data for this student
|
||||
Future<void> _inviteAllTeachersToAllAnalyticsRooms() async {
|
||||
final List<Future> inviteFutures = [];
|
||||
for (final Room analyticsRoom in allMyAnalyticsRooms) {
|
||||
inviteFutures.add(analyticsRoom.inviteTeachersToAnalyticsRoom());
|
||||
/// Invite teachers to all my analytics room.
|
||||
/// Handles case when students cannot add analytics room to space(s)
|
||||
/// so teacher is still able to get analytics data for this student
|
||||
void _inviteAllTeachersToAllAnalyticsRooms() {
|
||||
for (final Room room in allMyAnalyticsRooms) {
|
||||
room.inviteTeachersToAnalyticsRoom();
|
||||
}
|
||||
await Future.wait(inviteFutures);
|
||||
}
|
||||
|
||||
// Join all analytics rooms in all spaces
|
||||
// Allows teachers to join analytics rooms without being invited
|
||||
Future<void> _joinAnalyticsRoomsInAllSpaces() async {
|
||||
final List<Future> joinFutures = [];
|
||||
for (final Room space in (await _spacesImTeaching)) {
|
||||
joinFutures.add(space.joinAnalyticsRoomsInSpace());
|
||||
}
|
||||
await Future.wait(joinFutures);
|
||||
}
|
||||
|
||||
// Join invited analytics rooms
|
||||
// Checks for invites to any student analytics rooms
|
||||
// Handles case of analytics rooms that can't be added to some space(s)
|
||||
Future<void> _joinInvitedAnalyticsRooms() async {
|
||||
final List<Room> allRooms = List.from(rooms);
|
||||
for (final Room room in allRooms) {
|
||||
if (room.membership == Membership.invite && room.isAnalyticsRoom) {
|
||||
try {
|
||||
await room.join();
|
||||
} catch (err) {
|
||||
debugPrint("Failed to join analytics room ${room.id}");
|
||||
}
|
||||
}
|
||||
for (final Room space in _spacesImTeaching) {
|
||||
// Each call to joinAnalyticsRoomsInSpace calls getSpaceHierarchy, which has a
|
||||
// strict rate limit. So we wait a second between each call to prevent a 429 error.
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() => space.joinAnalyticsRoomsInSpace(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to join all relevant analytics rooms
|
||||
// and set up those rooms to be joined by relevant teachers
|
||||
Future<void> _migrateAnalyticsRooms() async {
|
||||
await _updateAnalyticsRoomVisibility();
|
||||
await _addAnalyticsRoomsToAllSpaces();
|
||||
await _inviteAllTeachersToAllAnalyticsRooms();
|
||||
await _joinInvitedAnalyticsRooms();
|
||||
await _joinAnalyticsRoomsInAllSpaces();
|
||||
/// Join invited analytics rooms.
|
||||
/// Checks for invites to any student analytics rooms.
|
||||
/// Handles case of analytics rooms that can't be added to some space(s).
|
||||
void _joinInvitedAnalyticsRooms() {
|
||||
Future.wait(
|
||||
rooms
|
||||
.where(
|
||||
(room) =>
|
||||
room.membership == Membership.invite && room.isAnalyticsRoom,
|
||||
)
|
||||
.map(
|
||||
(room) => room.join().catchError((err, s) {
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper function to join all relevant analytics rooms
|
||||
/// and set up those rooms to be joined by other users.
|
||||
void _migrateAnalyticsRooms() {
|
||||
_updateAnalyticsRoomVisibility().then((_) {
|
||||
_addAnalyticsRoomsToAllSpaces();
|
||||
_inviteAllTeachersToAllAnalyticsRooms();
|
||||
_joinInvitedAnalyticsRooms();
|
||||
_joinAnalyticsRoomsInAllSpaces();
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, DateTime?>> _allAnalyticsRoomsLastUpdated() async {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
|
|
@ -20,10 +19,15 @@ part "space_extension.dart";
|
|||
extension PangeaClient on Client {
|
||||
// analytics
|
||||
|
||||
/// Get the logged in user's analytics room matching
|
||||
/// a given langCode. If not present, create it.
|
||||
Future<Room> getMyAnalyticsRoom(String langCode) async =>
|
||||
await _getMyAnalyticsRoom(langCode);
|
||||
|
||||
Room? analyticsRoomLocal(String? langCode, [String? userIdParam]) =>
|
||||
/// Get local analytics room for a given langCode and
|
||||
/// optional userId (if not specified, uses current user).
|
||||
/// If user is invited to the room, joins the room.
|
||||
Room? analyticsRoomLocal(String langCode, [String? userIdParam]) =>
|
||||
_analyticsRoomLocal(langCode, userIdParam);
|
||||
|
||||
List<Room> get allMyAnalyticsRooms => _allMyAnalyticsRooms;
|
||||
|
|
@ -31,35 +35,24 @@ extension PangeaClient on Client {
|
|||
Future<void> updateAnalyticsRoomVisibility() async =>
|
||||
await _updateAnalyticsRoomVisibility();
|
||||
|
||||
Future<void> addAnalyticsRoomsToAllSpaces() async =>
|
||||
await _addAnalyticsRoomsToAllSpaces();
|
||||
|
||||
Future<void> inviteAllTeachersToAllAnalyticsRooms() async =>
|
||||
await _inviteAllTeachersToAllAnalyticsRooms();
|
||||
|
||||
Future<void> joinAnalyticsRoomsInAllSpaces() async =>
|
||||
await _joinAnalyticsRoomsInAllSpaces();
|
||||
|
||||
Future<void> joinInvitedAnalyticsRooms() async =>
|
||||
await _joinInvitedAnalyticsRooms();
|
||||
|
||||
Future<void> migrateAnalyticsRooms() async => await _migrateAnalyticsRooms();
|
||||
/// Helper function to join all relevant analytics rooms
|
||||
/// and set up those rooms to be joined by other users.
|
||||
void migrateAnalyticsRooms() => _migrateAnalyticsRooms();
|
||||
|
||||
Future<Map<String, DateTime?>> allAnalyticsRoomsLastUpdated() async =>
|
||||
await _allAnalyticsRoomsLastUpdated();
|
||||
|
||||
// spaces
|
||||
|
||||
Future<List<Room>> get spacesImTeaching async => await _spacesImTeaching;
|
||||
List<Room> get spacesImTeaching => _spacesImTeaching;
|
||||
|
||||
Future<List<Room>> get chatsImAStudentIn async => await _chatsImAStudentIn;
|
||||
|
||||
Future<List<Room>> get spaceImAStudentIn async => await _spacesImStudyingIn;
|
||||
List<Room> get spacesImAStudentIn => _spacesImStudyingIn;
|
||||
|
||||
List<Room> get spacesImIn => _spacesImIn;
|
||||
|
||||
Future<PangeaRoomRules?> get lastUpdatedRoomRules async =>
|
||||
await _lastUpdatedRoomRules;
|
||||
PangeaRoomRules? get lastUpdatedRoomRules => _lastUpdatedRoomRules;
|
||||
|
||||
// general_info
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,8 @@
|
|||
part of "client_extension.dart";
|
||||
|
||||
extension SpaceClientExtension on Client {
|
||||
Future<List<Room>> get _spacesImTeaching async {
|
||||
final allSpaces = rooms.where((room) => room.isSpace);
|
||||
for (final Room space in allSpaces) {
|
||||
if (space.getState(EventTypes.RoomPowerLevels) == null) {
|
||||
await space.postLoad();
|
||||
}
|
||||
}
|
||||
|
||||
final spaces = rooms
|
||||
.where(
|
||||
(e) =>
|
||||
(e.isSpace) &&
|
||||
e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
return spaces;
|
||||
}
|
||||
List<Room> get _spacesImTeaching =>
|
||||
rooms.where((e) => e.isSpace && e.isRoomAdmin).toList();
|
||||
|
||||
Future<List<Room>> get _chatsImAStudentIn async {
|
||||
final List<String> nowteacherRoomIds = await teacherRoomIds;
|
||||
|
|
@ -31,39 +16,18 @@ extension SpaceClientExtension on Client {
|
|||
.toList();
|
||||
}
|
||||
|
||||
Future<List<Room>> get _spacesImStudyingIn async {
|
||||
final List<Room> joinedSpaces = rooms
|
||||
.where(
|
||||
(room) => room.isSpace && room.membership == Membership.join,
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final Room space in joinedSpaces) {
|
||||
if (space.getState(EventTypes.RoomPowerLevels) == null) {
|
||||
await space.postLoad();
|
||||
}
|
||||
}
|
||||
|
||||
final spaces = rooms
|
||||
.where(
|
||||
(e) =>
|
||||
e.isSpace &&
|
||||
e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
return spaces;
|
||||
}
|
||||
List<Room> get _spacesImStudyingIn =>
|
||||
rooms.where((e) => e.isSpace && !e.isRoomAdmin).toList();
|
||||
|
||||
List<Room> get _spacesImIn => rooms.where((e) => e.isSpace).toList();
|
||||
|
||||
Future<PangeaRoomRules?> get _lastUpdatedRoomRules async =>
|
||||
(await _spacesImTeaching)
|
||||
.where((space) => space.rulesUpdatedAt != null)
|
||||
.sorted(
|
||||
(a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!),
|
||||
)
|
||||
.firstOrNull
|
||||
?.pangeaRoomRules;
|
||||
PangeaRoomRules? get _lastUpdatedRoomRules => _spacesImTeaching
|
||||
.where((space) => space.rulesUpdatedAt != null)
|
||||
.sorted(
|
||||
(a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!),
|
||||
)
|
||||
.firstOrNull
|
||||
?.pangeaRoomRules;
|
||||
|
||||
// LanguageSettingsModel? get _lastUpdatedLanguageSettings => rooms
|
||||
// .where((room) => room.isSpace && room.languageSettingsUpdatedAt != null)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
part of "pangea_room_extension.dart";
|
||||
|
||||
extension ChildrenAndParentsRoomExtension on Room {
|
||||
bool get _isSubspace => _pangeaSpaceParents.isNotEmpty;
|
||||
|
||||
//note this only will return rooms that the user has joined or been invited to
|
||||
List<Room> get _joinedChildren {
|
||||
if (!isSpace) return [];
|
||||
|
|
@ -91,7 +93,7 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
String _nameIncludingParents(BuildContext context) {
|
||||
String nameSoFar = getLocalizedDisplayname(MatrixLocals(L10n.of(context)!));
|
||||
Room currentRoom = this;
|
||||
if (currentRoom.pangeaSpaceParents.isEmpty) {
|
||||
if (!currentRoom._isSubspace) {
|
||||
return nameSoFar;
|
||||
}
|
||||
currentRoom = currentRoom.pangeaSpaceParents.first;
|
||||
|
|
@ -100,7 +102,7 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
nameToAdd =
|
||||
nameToAdd.length <= 10 ? nameToAdd : "${nameToAdd.substring(0, 10)}...";
|
||||
nameSoFar = '$nameToAdd > $nameSoFar';
|
||||
if (currentRoom.pangeaSpaceParents.isEmpty) {
|
||||
if (!currentRoom._isSubspace) {
|
||||
return nameSoFar;
|
||||
}
|
||||
return "... > $nameSoFar";
|
||||
|
|
@ -161,4 +163,14 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
await setSpaceChild(roomId, suggested: suggested);
|
||||
}
|
||||
}
|
||||
|
||||
/// A map of child suggestion status for a space.
|
||||
Map<String, bool> get _spaceChildSuggestionStatus {
|
||||
if (!isSpace) return {};
|
||||
final Map<String, bool> suggestionStatus = {};
|
||||
for (final child in spaceChildren) {
|
||||
suggestionStatus[child.roomId!] = child.suggested ?? true;
|
||||
}
|
||||
return suggestionStatus;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ part of "pangea_room_extension.dart";
|
|||
|
||||
extension EventsRoomExtension on Room {
|
||||
Future<bool> _leaveIfFull() async {
|
||||
await postLoad();
|
||||
if (!isRoomAdmin &&
|
||||
(_capacity != null) &&
|
||||
(await _numNonAdmins) > (_capacity!)) {
|
||||
|
|
|
|||
|
|
@ -49,26 +49,35 @@ part "user_permissions_extension.dart";
|
|||
extension PangeaRoom on Room {
|
||||
// analytics
|
||||
|
||||
/// Join analytics rooms in space.
|
||||
/// Allows teachers to join analytics rooms without being invited.
|
||||
Future<void> joinAnalyticsRoomsInSpace() async =>
|
||||
await _joinAnalyticsRoomsInSpace();
|
||||
|
||||
Future<void> addAnalyticsRoomToSpace(Room analyticsRoom) async =>
|
||||
await _addAnalyticsRoomToSpace(analyticsRoom);
|
||||
|
||||
Future<void> addAnalyticsRoomToSpaces() async =>
|
||||
await _addAnalyticsRoomToSpaces();
|
||||
/// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces).
|
||||
/// Enables teachers to join student analytics rooms via space hierarchy.
|
||||
/// Will not always work, as there may be spaces where students don't have permission to add chats,
|
||||
/// but allows teachers to join analytics rooms without being invited.
|
||||
void addAnalyticsRoomToSpaces() => _addAnalyticsRoomToSpaces();
|
||||
|
||||
Future<void> addAnalyticsRoomsToSpace() async =>
|
||||
await _addAnalyticsRoomsToSpace();
|
||||
/// Add all the user's analytics rooms to 1 space.
|
||||
void addAnalyticsRoomsToSpace() => _addAnalyticsRoomsToSpace();
|
||||
|
||||
/// Invite teachers of 1 space to 1 analytics room
|
||||
Future<void> inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async =>
|
||||
await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom);
|
||||
|
||||
Future<void> inviteTeachersToAnalyticsRoom() async =>
|
||||
await _inviteTeachersToAnalyticsRoom();
|
||||
/// Invite all the user's teachers to 1 analytics room.
|
||||
/// Handles case when students cannot add analytics room to space
|
||||
/// so teacher is still able to get analytics data for this student.
|
||||
void inviteTeachersToAnalyticsRoom() => _inviteTeachersToAnalyticsRoom();
|
||||
|
||||
Future<void> inviteSpaceTeachersToAnalyticsRooms() async =>
|
||||
await _inviteSpaceTeachersToAnalyticsRooms();
|
||||
/// Invite teachers of 1 space to all users' analytics rooms
|
||||
void inviteSpaceTeachersToAnalyticsRooms() =>
|
||||
_inviteSpaceTeachersToAnalyticsRooms();
|
||||
|
||||
Future<AnalyticsEvent?> getLastAnalyticsEvent(
|
||||
String type,
|
||||
|
|
@ -122,6 +131,19 @@ extension PangeaRoom on Room {
|
|||
}) async =>
|
||||
await _pangeaSetSpaceChild(roomId, suggested: suggested);
|
||||
|
||||
/// Returns a map of child suggestion status for a space.
|
||||
///
|
||||
/// If the current object is not a space, an empty map is returned.
|
||||
/// Otherwise, it iterates through each child in the `spaceChildren` list
|
||||
/// and adds their suggestion status to the `suggestionStatus` map.
|
||||
/// The suggestion status is determined by the `suggested` property of each child.
|
||||
/// If the `suggested` property is `null`, it defaults to `true`.
|
||||
Map<String, bool> get spaceChildSuggestionStatus =>
|
||||
_spaceChildSuggestionStatus;
|
||||
|
||||
/// Checks if this space has a parent space
|
||||
bool get isSubspace => _isSubspace;
|
||||
|
||||
// class_and_exchange_settings
|
||||
|
||||
DateTime? get rulesUpdatedAt => _rulesUpdatedAt;
|
||||
|
|
@ -134,6 +156,12 @@ extension PangeaRoom on Room {
|
|||
|
||||
Future<List<User>> get teachers async => await _teachers;
|
||||
|
||||
/// Synchronous version of teachers getter. Does not request
|
||||
/// participants, so this list may not be complete.
|
||||
List<User> get teachersLocal => _teachersLocal;
|
||||
|
||||
/// If the user is an admin of this space, and the space's
|
||||
/// m.space.child power level hasn't yet been set, so it to 0
|
||||
Future<void> setClassPowerLevels() async => await _setClassPowerLevels();
|
||||
|
||||
Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent;
|
||||
|
|
|
|||
|
|
@ -1,57 +1,44 @@
|
|||
part of "pangea_room_extension.dart";
|
||||
|
||||
extension AnalyticsRoomExtension on Room {
|
||||
// Join analytics rooms in space
|
||||
// Allows teachers to join analytics rooms without being invited
|
||||
/// Join analytics rooms in space.
|
||||
/// Allows teachers to join analytics rooms without being invited.
|
||||
Future<void> _joinAnalyticsRoomsInSpace() async {
|
||||
if (!isSpace) {
|
||||
debugPrint("joinAnalyticsRoomsInSpace called on non-space room");
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "joinAnalyticsRoomsInSpace called on non-space room",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// added delay because without it power levels don't load and user is not
|
||||
// recognized as admin
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
await postLoad();
|
||||
|
||||
if (!isRoomAdmin) {
|
||||
debugPrint("joinAnalyticsRoomsInSpace called by non-admin");
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "joinAnalyticsRoomsInSpace called by non-admin",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final spaceHierarchy = await client.getSpaceHierarchy(
|
||||
id,
|
||||
maxDepth: 1,
|
||||
);
|
||||
|
||||
final List<String> analyticsRoomIds = spaceHierarchy.rooms
|
||||
.where(
|
||||
(r) => r.roomType == PangeaRoomTypes.analytics,
|
||||
)
|
||||
.map((r) => r.roomId)
|
||||
.toList();
|
||||
|
||||
for (final String roomID in analyticsRoomIds) {
|
||||
try {
|
||||
await joinSpaceChild(roomID);
|
||||
} catch (err, s) {
|
||||
debugPrint("Failed to join analytics room $roomID in space $id");
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to join analytics room $roomID in space $id",
|
||||
s: s,
|
||||
);
|
||||
try {
|
||||
if (!isSpace) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRoomAdmin) return;
|
||||
final spaceHierarchy = await client.getSpaceHierarchy(
|
||||
id,
|
||||
maxDepth: 1,
|
||||
);
|
||||
|
||||
final List<String> analyticsRoomIds = spaceHierarchy.rooms
|
||||
.where((r) => r.roomType == PangeaRoomTypes.analytics)
|
||||
.map((r) => r.roomId)
|
||||
.toList();
|
||||
|
||||
await Future.wait(
|
||||
analyticsRoomIds.map(
|
||||
(roomID) => joinSpaceChild(roomID).catchError((err, s) {
|
||||
debugPrint("Failed to join analytics room $roomID in space $id");
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to join analytics room $roomID in space $id",
|
||||
s: s,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,107 +71,70 @@ extension AnalyticsRoomExtension on Room {
|
|||
}
|
||||
}
|
||||
|
||||
// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces)
|
||||
// So teachers can join them via space hierarchy
|
||||
// Will not always work, as there may be spaces where students don't have permission to add chats
|
||||
// But allows teachers to join analytics rooms without being invited
|
||||
Future<void> _addAnalyticsRoomToSpaces() async {
|
||||
if (!isAnalyticsRoomOfUser(client.userID!)) {
|
||||
debugPrint("addAnalyticsRoomToSpaces called on non-analytics room");
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "addAnalyticsRoomToSpaces called on non-analytics room",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (final Room space in (await client.spaceImAStudentIn)) {
|
||||
if (space.spaceChildren.any((sc) => sc.roomId == id)) continue;
|
||||
await space.addAnalyticsRoomToSpace(this);
|
||||
}
|
||||
/// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces).
|
||||
/// Enables teachers to join student analytics rooms via space hierarchy.
|
||||
/// Will not always work, as there may be spaces where students don't have permission to add chats,
|
||||
/// but allows teachers to join analytics rooms without being invited.
|
||||
void _addAnalyticsRoomToSpaces() {
|
||||
if (!isAnalyticsRoomOfUser(client.userID!)) return;
|
||||
Future.wait(
|
||||
client.spacesImAStudentIn
|
||||
.where((space) => !space.spaceChildren.any((sc) => sc.roomId == id))
|
||||
.map((space) => space.addAnalyticsRoomToSpace(this)),
|
||||
);
|
||||
}
|
||||
|
||||
// Add all analytics rooms to space
|
||||
// Similar to addAnalyticsRoomToSpaces, but all analytics room to 1 space
|
||||
Future<void> _addAnalyticsRoomsToSpace() async {
|
||||
await postLoad();
|
||||
final List<Room> allMyAnalyticsRooms = client.allMyAnalyticsRooms;
|
||||
for (final Room analyticsRoom in allMyAnalyticsRooms) {
|
||||
await addAnalyticsRoomToSpace(analyticsRoom);
|
||||
}
|
||||
/// Add all the user's analytics rooms to 1 space.
|
||||
void _addAnalyticsRoomsToSpace() {
|
||||
Future.wait(
|
||||
client.allMyAnalyticsRooms.map((room) => addAnalyticsRoomToSpace(room)),
|
||||
);
|
||||
}
|
||||
|
||||
// invite teachers of 1 space to 1 analytics room
|
||||
/// Invite teachers of 1 space to 1 analytics room
|
||||
Future<void> _inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async {
|
||||
if (!isSpace) {
|
||||
debugPrint(
|
||||
"inviteSpaceTeachersToAnalyticsRoom called on non-space room",
|
||||
);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message:
|
||||
"inviteSpaceTeachersToAnalyticsRoom called on non-space room",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!isSpace) return;
|
||||
if (!analyticsRoom.participantListComplete) {
|
||||
await analyticsRoom.requestParticipants();
|
||||
}
|
||||
|
||||
final List<User> participants = analyticsRoom.getParticipants();
|
||||
for (final User teacher in (await teachers)) {
|
||||
if (!participants.any((p) => p.id == teacher.id)) {
|
||||
try {
|
||||
await analyticsRoom.invite(teacher.id);
|
||||
} catch (err, s) {
|
||||
debugPrint(
|
||||
"Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}",
|
||||
);
|
||||
final List<User> uninvitedTeachers = teachersLocal
|
||||
.where((teacher) => !participants.contains(teacher))
|
||||
.toList();
|
||||
|
||||
Future.wait(
|
||||
uninvitedTeachers.map(
|
||||
(teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}",
|
||||
s: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Invite all teachers to 1 analytics room
|
||||
// Handles case when students cannot add analytics room to space
|
||||
// So teacher is still able to get analytics data for this student
|
||||
Future<void> _inviteTeachersToAnalyticsRoom() async {
|
||||
if (client.userID == null) {
|
||||
debugPrint("inviteTeachersToAnalyticsRoom called with null userId");
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "inviteTeachersToAnalyticsRoom called with null userId",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAnalyticsRoomOfUser(client.userID!)) {
|
||||
debugPrint("inviteTeachersToAnalyticsRoom called on non-analytics room");
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "inviteTeachersToAnalyticsRoom called on non-analytics room",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (final Room space in (await client.spaceImAStudentIn)) {
|
||||
await space.inviteSpaceTeachersToAnalyticsRoom(this);
|
||||
}
|
||||
/// Invite all the user's teachers to 1 analytics room.
|
||||
/// Handles case when students cannot add analytics room to space
|
||||
/// so teacher is still able to get analytics data for this student.
|
||||
void _inviteTeachersToAnalyticsRoom() {
|
||||
if (client.userID == null || !isAnalyticsRoomOfUser(client.userID!)) return;
|
||||
Future.wait(
|
||||
client.spacesImAStudentIn.map(
|
||||
(space) => inviteSpaceTeachersToAnalyticsRoom(this),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Invite teachers of 1 space to all users' analytics rooms
|
||||
Future<void> _inviteSpaceTeachersToAnalyticsRooms() async {
|
||||
for (final Room analyticsRoom in client.allMyAnalyticsRooms) {
|
||||
await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom);
|
||||
}
|
||||
/// Invite teachers of 1 space to all users' analytics rooms
|
||||
void _inviteSpaceTeachersToAnalyticsRooms() {
|
||||
Future.wait(
|
||||
client.allMyAnalyticsRooms.map(
|
||||
(room) => inviteSpaceTeachersToAnalyticsRoom(room),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<AnalyticsEvent?> _getLastAnalyticsEvent(
|
||||
|
|
|
|||
|
|
@ -55,27 +55,39 @@ extension SpaceRoomExtension on Room {
|
|||
: participants;
|
||||
}
|
||||
|
||||
/// Synchronous version of _teachers. Does not request participants, so this list may not be complete.
|
||||
List<User> get _teachersLocal {
|
||||
if (!isSpace) return [];
|
||||
return getParticipants()
|
||||
.where(
|
||||
(e) =>
|
||||
e.powerLevel == ClassDefaultValues.powerLevelOfAdmin &&
|
||||
e.id != BotName.byEnvironment,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// If the user is an admin of this space, and the space's
|
||||
/// m.space.child power level hasn't yet been set, so it to 0
|
||||
Future<void> _setClassPowerLevels() async {
|
||||
try {
|
||||
if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) {
|
||||
return;
|
||||
}
|
||||
if (!isRoomAdmin) return;
|
||||
final dynamic currentPower = getState(EventTypes.RoomPowerLevels);
|
||||
if (currentPower is! Event?) {
|
||||
return;
|
||||
}
|
||||
final Map<String, dynamic>? currentPowerContent =
|
||||
if (currentPower is! Event?) return;
|
||||
|
||||
final currentPowerContent =
|
||||
currentPower?.content["events"] as Map<String, dynamic>?;
|
||||
final spaceChildPower = currentPowerContent?[EventTypes.SpaceChild];
|
||||
|
||||
if (spaceChildPower == null && currentPowerContent != null) {
|
||||
currentPowerContent["events"][EventTypes.SpaceChild] = 0;
|
||||
currentPowerContent[EventTypes.SpaceChild] = 0;
|
||||
currentPower!.content["events"] = currentPowerContent;
|
||||
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
EventTypes.RoomPowerLevels,
|
||||
currentPower?.stateKey ?? "",
|
||||
currentPowerContent,
|
||||
currentPower.stateKey ?? "",
|
||||
currentPower.content,
|
||||
);
|
||||
}
|
||||
} catch (err, s) {
|
||||
|
|
|
|||
|
|
@ -82,10 +82,9 @@ class PangeaMessageEvent {
|
|||
.firstOrNull ??
|
||||
_event;
|
||||
|
||||
Event updateLatestEdit() {
|
||||
void updateLatestEdit() {
|
||||
_latestEditCache = null;
|
||||
_representations = null;
|
||||
return _latestEdit;
|
||||
}
|
||||
|
||||
Future<PangeaAudioFile> getMatrixAudioFile(
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
|
|||
|
||||
Future<void> getChatAndStudents() async {
|
||||
try {
|
||||
await spaceRoom?.postLoad();
|
||||
await spaceRoom?.requestParticipants();
|
||||
|
||||
if (spaceRoom != null) {
|
||||
|
|
|
|||
|
|
@ -49,15 +49,8 @@ class StudentAnalyticsController extends State<StudentAnalyticsPage> {
|
|||
return _chats;
|
||||
}
|
||||
|
||||
List<Room> _spaces = [];
|
||||
List<Room> get spaces {
|
||||
if (_spaces.isEmpty) {
|
||||
_pangeaController.matrixState.client.spaceImAStudentIn.then((result) {
|
||||
setState(() => _spaces = result);
|
||||
});
|
||||
}
|
||||
return _spaces;
|
||||
}
|
||||
List<Room> get spaces =>
|
||||
_pangeaController.matrixState.client.spacesImAStudentIn;
|
||||
|
||||
String? get userId {
|
||||
final id = _pangeaController.matrixState.client.userID;
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class MeasurableWidget extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Function(Size? size, Offset? position) onChange;
|
||||
|
||||
const MeasurableWidget({
|
||||
super.key,
|
||||
required this.onChange,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
_WidgetSizeState createState() => _WidgetSizeState();
|
||||
}
|
||||
|
||||
class _WidgetSizeState extends State<MeasurableWidget> {
|
||||
var widgetKey = GlobalKey();
|
||||
Offset? oldPosition;
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void postFrameCallback(_) {
|
||||
final context = widgetKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final RenderBox? box =
|
||||
widgetKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
|
||||
if (box != null && box.hasSize) {
|
||||
final Offset position = box.localToGlobal(Offset.zero);
|
||||
|
||||
if (oldPosition != null) {
|
||||
if (oldPosition!.dx == position.dx && oldPosition!.dy == position.dy) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
oldPosition = position;
|
||||
|
||||
final newSize = context.size;
|
||||
widget.onChange(newSize, position);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);
|
||||
return Container(
|
||||
key: widgetKey,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,6 @@ void chatListHandleSpaceTap(
|
|||
if (await space.leaveIfFull()) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
await space.postLoad();
|
||||
setActiveSpaceAndCloseChat();
|
||||
},
|
||||
onError: (exception) {
|
||||
|
|
@ -72,7 +71,7 @@ void chatListHandleSpaceTap(
|
|||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
if (space.isSpace) {
|
||||
await space.joinAnalyticsRoomsInSpace();
|
||||
space.joinAnalyticsRoomsInSpace();
|
||||
}
|
||||
setActiveSpaceAndCloseChat();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
|
|||
94
lib/pangea/widgets/chat/chat_floating_action_button.dart
Normal file
94
lib/pangea/widgets/chat/chat_floating_action_button.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChatFloatingActionButton extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
const ChatFloatingActionButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
ChatFloatingActionButtonState createState() =>
|
||||
ChatFloatingActionButtonState();
|
||||
}
|
||||
|
||||
class ChatFloatingActionButtonState extends State<ChatFloatingActionButton> {
|
||||
bool showPermissionsError = false;
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final permissionsController =
|
||||
widget.controller.pangeaController.permissionsController;
|
||||
final itEnabled = permissionsController.isToolEnabled(
|
||||
ToolSetting.interactiveTranslator,
|
||||
widget.controller.room,
|
||||
);
|
||||
final igcEnabled = permissionsController.isToolEnabled(
|
||||
ToolSetting.interactiveGrammar,
|
||||
widget.controller.room,
|
||||
);
|
||||
showPermissionsError = !itEnabled || !igcEnabled;
|
||||
debugPrint("showPermissionsError: $showPermissionsError");
|
||||
|
||||
if (showPermissionsError) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 5),
|
||||
() {
|
||||
if (mounted) setState(() => showPermissionsError = false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Rebuild the widget each time there's an update from choreo (i.e., an error).
|
||||
_choreoSub =
|
||||
widget.controller.choreographer.stateListener.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.controller.selectedEvents.isNotEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (widget.controller.showScrollDownButton) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 56.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: widget.controller.scrollDown,
|
||||
heroTag: null,
|
||||
mini: true,
|
||||
child: const Icon(Icons.arrow_downward_outlined),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (widget.controller.choreographer.errorService.error != null) {
|
||||
return ChoreographerHasErrorButton(
|
||||
widget.controller.pangeaController,
|
||||
widget.controller.choreographer.errorService.error!,
|
||||
);
|
||||
}
|
||||
|
||||
return showPermissionsError
|
||||
? LanguagePermissionsButtons(
|
||||
choreographer: widget.controller.choreographer,
|
||||
roomID: widget.controller.roomId,
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
82
lib/pangea/widgets/chat/input_bar_wrapper.dart
Normal file
82
lib/pangea/widgets/chat/input_bar_wrapper.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fluffychat/pages/chat/input_bar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class InputBarWrapper extends StatefulWidget {
|
||||
final Room room;
|
||||
final int? minLines;
|
||||
final int? maxLines;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final ValueChanged<Uint8List?>? onSubmitImage;
|
||||
final FocusNode? focusNode;
|
||||
final PangeaTextController? controller;
|
||||
final InputDecoration? decoration;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final bool? autofocus;
|
||||
final bool readOnly;
|
||||
|
||||
const InputBarWrapper({
|
||||
required this.room,
|
||||
this.minLines,
|
||||
this.maxLines,
|
||||
this.keyboardType,
|
||||
this.onSubmitted,
|
||||
this.onSubmitImage,
|
||||
this.focusNode,
|
||||
this.controller,
|
||||
this.decoration,
|
||||
this.onChanged,
|
||||
this.autofocus,
|
||||
this.textInputAction,
|
||||
this.readOnly = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InputBarWrapper> createState() => InputBarWrapperState();
|
||||
}
|
||||
|
||||
class InputBarWrapperState extends State<InputBarWrapper> {
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Rebuild the widget each time there's an update from choreo
|
||||
_choreoSub =
|
||||
widget.controller?.choreographer.stateListener.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InputBar(
|
||||
room: widget.room,
|
||||
minLines: widget.minLines,
|
||||
maxLines: widget.maxLines,
|
||||
keyboardType: widget.keyboardType,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onSubmitImage: widget.onSubmitImage,
|
||||
focusNode: widget.focusNode,
|
||||
controller: widget.controller,
|
||||
decoration: widget.decoration,
|
||||
onChanged: widget.onChanged,
|
||||
autofocus: widget.autofocus,
|
||||
textInputAction: widget.textInputAction,
|
||||
readOnly: widget.readOnly,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/text_to_speech_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PangeaMessageActions extends StatelessWidget {
|
||||
final ChatController chatController;
|
||||
|
||||
const PangeaMessageActions({super.key, required this.chatController});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return chatController.selectedEvents.length == 1
|
||||
? Row(
|
||||
children: <Widget>[
|
||||
// LanguageToggleSwitch(controller: chatController),
|
||||
TextToSpeechButton(
|
||||
controller: chatController,
|
||||
selectedEvent: chatController.selectedEvents.first,
|
||||
),
|
||||
// IconButton(
|
||||
// icon: Icon(Icons.mic),
|
||||
// onPressed: chatController.onMicTap,
|
||||
// ),
|
||||
// Add more IconButton widgets here
|
||||
],
|
||||
)
|
||||
: const SizedBox();
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator
|
|||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class MessageTranslationCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
|
|
@ -140,9 +141,15 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
return const CardErrorWidget();
|
||||
}
|
||||
|
||||
final bool showWarning = l2Code != null &&
|
||||
!widget.immersionMode &&
|
||||
widget.messageEvent.originalSent?.langCode != l2Code &&
|
||||
// Show warning if message's language code is user's L1
|
||||
// or if translated text is same as original text
|
||||
// Warning does not show if was previously closed
|
||||
final bool showWarning = widget.messageEvent.originalSent != null &&
|
||||
((!widget.immersionMode &&
|
||||
widget.messageEvent.originalSent!.langCode.equals(l1Code)) ||
|
||||
(selectionTranslation == null ||
|
||||
widget.messageEvent.originalSent!.text
|
||||
.equals(selectionTranslation))) &&
|
||||
!MatrixState.pangeaController.instructions.wereInstructionsTurnedOff(
|
||||
InlineInstructions.l1Translation.toString(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class TextToSpeechButton extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
final Event selectedEvent;
|
||||
|
||||
const TextToSpeechButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.selectedEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
_TextToSpeechButtonState createState() => _TextToSpeechButtonState();
|
||||
}
|
||||
|
||||
class _TextToSpeechButtonState extends State<TextToSpeechButton> {
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
late PangeaMessageEvent _pangeaMessageEvent;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_audioPlayer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pangeaMessageEvent = PangeaMessageEvent(
|
||||
event: widget.selectedEvent,
|
||||
timeline: widget.controller.timeline!,
|
||||
ownMessage:
|
||||
widget.selectedEvent.senderId == Matrix.of(context).client.userID,
|
||||
);
|
||||
}
|
||||
|
||||
Event? get localAudioEvent =>
|
||||
langCode != null && text != null && text!.isNotEmpty
|
||||
? _pangeaMessageEvent.getTextToSpeechLocal(langCode!, text!)
|
||||
: null;
|
||||
|
||||
String? get langCode =>
|
||||
widget.controller.choreographer.messageOptions.selectedDisplayLang
|
||||
?.langCode ??
|
||||
widget.controller.choreographer.l2LangCode;
|
||||
|
||||
String? get text => langCode != null
|
||||
? _pangeaMessageEvent.representationByLanguage(langCode!)?.text
|
||||
: null;
|
||||
|
||||
Future<void> _getAudio() async {
|
||||
try {
|
||||
if (!mounted) return;
|
||||
if (text == null || text!.isEmpty) return;
|
||||
if (langCode == null || langCode!.isEmpty) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
await _pangeaMessageEvent.getTextToSpeechGlobal(langCode!);
|
||||
setState(() => _isLoading = false);
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
debugger(when: kDebugMode);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.errorGettingAudio),
|
||||
),
|
||||
);
|
||||
ErrorHandler.logError(
|
||||
e: Exception(),
|
||||
s: StackTrace.current,
|
||||
m: 'text is null or empty in text_to_speech_button.dart',
|
||||
data: {'selectedEvent': widget.selectedEvent, 'langCode': langCode},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final playButton = InkWell(
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
onTap: text == null || text!.isEmpty ? null : _getAudio,
|
||||
child: Material(
|
||||
color: AppConfig.primaryColor.withAlpha(64),
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
child: const Icon(
|
||||
// Change the icon based on some condition. If you have an audio player state, use it here.
|
||||
Icons.play_arrow_outlined,
|
||||
color: AppConfig.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return localAudioEvent == null
|
||||
? Opacity(
|
||||
opacity: text == null || text!.isEmpty ? 0.5 : 1,
|
||||
child: SizedBox(
|
||||
width: 44, // Match the size of the button in AudioPlayerState
|
||||
height: 36,
|
||||
child: Padding(
|
||||
//only left side of the button is padded to match the padding of the AudioPlayerState
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: playButton,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 250,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
AudioPlayerWidget(
|
||||
localAudioEvent!,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -111,12 +111,12 @@ abstract class ClientManager {
|
|||
// To make room emotes work
|
||||
'im.ponies.room_emotes',
|
||||
// #Pangea
|
||||
PangeaEventTypes.languageSettings,
|
||||
// The things in this list will be loaded in the first sync, without having
|
||||
// to postLoad to confirm that these state events are completely loaded
|
||||
PangeaEventTypes.rules,
|
||||
PangeaEventTypes.botOptions,
|
||||
EventTypes.RoomTopic,
|
||||
EventTypes.RoomAvatar,
|
||||
PangeaEventTypes.capacity,
|
||||
EventTypes.RoomPowerLevels,
|
||||
// Pangea#
|
||||
},
|
||||
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:badges/badges.dart' as b;
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'matrix.dart';
|
||||
|
|
@ -24,7 +24,10 @@ class UnreadRoomsBadge extends StatelessWidget {
|
|||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where((syncUpdate) => syncUpdate.hasRoomUpdate),
|
||||
.where((syncUpdate) => syncUpdate.hasRoomUpdate)
|
||||
// #Pangea
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
// Pangea#
|
||||
builder: (context, _) {
|
||||
// #Pangea
|
||||
// final unreadCount = Matrix.of(context)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue