diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 9337b488a..84a802e54 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -504,6 +504,9 @@ class InputBar extends StatelessWidget { onSubmitted!(text); }, // #Pangea + style: controller?.isMaxLength ?? false + ? const TextStyle(color: Colors.red) + : null, onTap: () { controller!.onInputTap( context, diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index d0ff8cdf5..ccc9aa9c4 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -53,6 +53,25 @@ class _SpaceViewState extends State { widget.controller.pangeaController.pStoreService.read(_chatCountsKey) ?? {}, ); + + /// Used to filter out sync updates with hierarchy updates for the active + /// space so that the view can be auto-reloaded in the room subscription + bool hasHierarchyUpdate(SyncUpdate update) { + final joinTimeline = + update.rooms?.join?[widget.controller.activeSpaceId]?.timeline; + final leaveTimeline = + update.rooms?.leave?[widget.controller.activeSpaceId]?.timeline; + if (joinTimeline == null && leaveTimeline == null) return false; + final bool hasJoinUpdate = joinTimeline?.events?.any( + (event) => event.type == EventTypes.SpaceChild, + ) ?? + false; + final bool hasLeaveUpdate = leaveTimeline?.events?.any( + (event) => event.type == EventTypes.SpaceChild, + ) ?? + false; + return hasJoinUpdate || hasLeaveUpdate; + } // Pangea# @override @@ -78,12 +97,9 @@ class _SpaceViewState extends State { // 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); - }); + _roomSubscription ??= client.onSync.stream + .where(hasHierarchyUpdate) + .listen((update) => loadHierarchy(hasUpdate: true)); // Pangea# super.initState(); } diff --git a/lib/pangea/controllers/permissions_controller.dart b/lib/pangea/controllers/permissions_controller.dart index ed84e13c3..3a8eca775 100644 --- a/lib/pangea/controllers/permissions_controller.dart +++ b/lib/pangea/controllers/permissions_controller.dart @@ -1,5 +1,4 @@ import 'package:fluffychat/pangea/constants/age_limits.dart'; -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; @@ -36,63 +35,73 @@ class PermissionsController extends BaseController { return dob?.isAtLeastYearsOld(AgeLimits.toAccessFeatures) ?? false; } - /// A user can private chat if - /// 1) they are 18 and outside a class context or - /// 2) they are in a class context and the class rules permit it - /// If no class is passed, uses classController.activeClass + /// A user can private chat if they are 18+ bool canUserPrivateChat({String? roomID}) { - final Room? classContext = - firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); - return classContext?.pangeaRoomRules == null - ? isUser18() - : classContext!.pangeaRoomRules!.oneToOneChatClass || - classContext.isRoomAdmin; + return isUser18(); + // Rules can't be edited; default to true + // final Room? classContext = + // firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); + // return classContext?.pangeaRoomRules == null + // ? isUser18() + // : classContext!.pangeaRoomRules!.oneToOneChatClass || + // classContext.isRoomAdmin; } bool canUserGroupChat({String? roomID}) { - final Room? classContext = - firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); + return isUser18(); + // Rules can't be edited; default to true + // final Room? classContext = + // firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); - return classContext?.pangeaRoomRules == null - ? isUser18() - : classContext!.pangeaRoomRules!.isCreateRooms || - classContext.isRoomAdmin; + // return classContext?.pangeaRoomRules == null + // ? isUser18() + // : classContext!.pangeaRoomRules!.isCreateRooms || + // classContext.isRoomAdmin; } bool showChatInputAddButton(String roomId) { - final PangeaRoomRules? perms = _getRoomRules(roomId); - if (perms == null) return isUser18(); - return perms.isShareFiles || - perms.isShareLocation || - perms.isSharePhoto || - perms.isShareVideo; + // Rules can't be edited; default to true + // final PangeaRoomRules? perms = _getRoomRules(roomId); + // if (perms == null) return isUser18(); + // return perms.isShareFiles || + // perms.isShareLocation || + // perms.isSharePhoto || + // perms.isShareVideo; + return isUser18(); } /// works for both roomID of chat and class - bool canShareVideo(String? roomID) => - _getRoomRules(roomID)?.isShareVideo ?? isUser18(); + bool canShareVideo(String? roomID) => isUser18(); + // Rules can't be edited; default to true + // _getRoomRules(roomID)?.isShareVideo ?? isUser18(); /// works for both roomID of chat and class - bool canSharePhoto(String? roomID) => - _getRoomRules(roomID)?.isSharePhoto ?? isUser18(); + bool canSharePhoto(String? roomID) => isUser18(); + // Rules can't be edited; default to true + // _getRoomRules(roomID)?.isSharePhoto ?? isUser18(); /// works for both roomID of chat and class - bool canShareFile(String? roomID) => - _getRoomRules(roomID)?.isShareFiles ?? isUser18(); + bool canShareFile(String? roomID) => isUser18(); + // Rules can't be edited; default to true + // _getRoomRules(roomID)?.isShareFiles ?? isUser18(); /// works for both roomID of chat and class - bool canShareLocation(String? roomID) => - _getRoomRules(roomID)?.isShareLocation ?? isUser18(); + bool canShareLocation(String? roomID) => isUser18(); + // Rules can't be edited; default to true + // _getRoomRules(roomID)?.isShareLocation ?? isUser18(); - int? classLanguageToolPermission(Room room, ToolSetting setting) => - room.firstRules?.getToolSettings(setting); + int? classLanguageToolPermission(Room room, ToolSetting setting) => 1; + // Rules can't be edited; default to student choice + // room.firstRules?.getToolSettings(setting); - //what happens if a room isn't in a class? + // what happens if a room isn't in a class? bool isToolDisabledByClass(ToolSetting setting, Room? room) { - if (room?.isSpaceAdmin ?? false) return false; - final int? classPermission = - room != null ? classLanguageToolPermission(room, setting) : 1; - return classPermission == 0; + return false; + // Rules can't be edited; default to false + // if (room?.isSpaceAdmin ?? false) return false; + // final int? classPermission = + // room != null ? classLanguageToolPermission(room, setting) : 1; + // return classPermission == 0; } bool userToolSetting(ToolSetting setting) { @@ -117,18 +126,22 @@ class PermissionsController extends BaseController { } bool isToolEnabled(ToolSetting setting, Room? room) { - if (room?.isSpaceAdmin ?? false) { - return userToolSetting(setting); - } - final int? classPermission = - room != null ? classLanguageToolPermission(room, setting) : 1; - if (classPermission == 0) return false; - if (classPermission == 2) return true; + // Rules can't be edited; default to true return userToolSetting(setting); + // if (room?.isSpaceAdmin ?? false) { + // return userToolSetting(setting); + // } + // final int? classPermission = + // room != null ? classLanguageToolPermission(room, setting) : 1; + // if (classPermission == 0) return false; + // if (classPermission == 2) return true; + // return userToolSetting(setting); } bool isWritingAssistanceEnabled(Room? room) { - return isToolEnabled(ToolSetting.interactiveTranslator, room) && - isToolEnabled(ToolSetting.interactiveGrammar, room); + // Rules can't be edited; default to true + return true; + // return isToolEnabled(ToolSetting.interactiveTranslator, room) && + // isToolEnabled(ToolSetting.interactiveGrammar, room); } } diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index a0d6c21c9..3a2e62c86 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -54,6 +54,7 @@ extension AnalyticsRoomExtension on Room { return Future.value(); } + // Checks that user has permission to add child to space if (!canSendEvent(EventTypes.SpaceChild)) return; if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return; @@ -103,17 +104,19 @@ extension AnalyticsRoomExtension on Room { .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, - ); - }), - ), - ); + if (analyticsRoom.canSendEvent(EventTypes.RoomMember)) { + 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 the user's teachers to 1 analytics room. diff --git a/lib/pangea/utils/p_store.dart b/lib/pangea/utils/p_store.dart index 0dfe0d5cd..233975fc5 100644 --- a/lib/pangea/utils/p_store.dart +++ b/lib/pangea/utils/p_store.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; /// Utility to save and read data both in the matrix profile (this is the default /// behavior) and in the local storage (local needs to be specificied). An @@ -66,6 +67,9 @@ class PStore { /// Clears the storage by erasing all data in the box. void clearStorage() { + // this could potenitally be interfering with openning database + // at the start of the session, which is causing auto log outs on iOS + Sentry.addBreadcrumb(Breadcrumb(message: 'Clearing local storage')); _box.erase(); } } diff --git a/lib/pangea/widgets/chat/input_bar_wrapper.dart b/lib/pangea/widgets/chat/input_bar_wrapper.dart index 374f60a80..9441312bd 100644 --- a/lib/pangea/widgets/chat/input_bar_wrapper.dart +++ b/lib/pangea/widgets/chat/input_bar_wrapper.dart @@ -44,6 +44,7 @@ class InputBarWrapper extends StatefulWidget { class InputBarWrapperState extends State { StreamSubscription? _choreoSub; + String _currentText = ''; @override void initState() { @@ -61,6 +62,24 @@ class InputBarWrapperState extends State { super.dispose(); } + void refreshOnChange(String text) { + if (widget.onChanged != null) { + widget.onChanged!(text); + } + + final bool decreasedFromMaxLength = + _currentText.length >= PangeaTextController.maxLength && + text.length < PangeaTextController.maxLength; + final bool reachedMaxLength = + _currentText.length < PangeaTextController.maxLength && + text.length < PangeaTextController.maxLength; + + if (decreasedFromMaxLength || reachedMaxLength) { + setState(() {}); + } + _currentText = text; + } + @override Widget build(BuildContext context) { return InputBar( @@ -73,7 +92,7 @@ class InputBarWrapperState extends State { focusNode: widget.focusNode, controller: widget.controller, decoration: widget.decoration, - onChanged: widget.onChanged, + onChanged: refreshOnChange, autofocus: widget.autofocus, textInputAction: widget.textInputAction, readOnly: widget.readOnly, diff --git a/lib/pangea/widgets/igc/pangea_text_controller.dart b/lib/pangea/widgets/igc/pangea_text_controller.dart index b91186c22..8fc136edd 100644 --- a/lib/pangea/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/widgets/igc/pangea_text_controller.dart @@ -25,6 +25,10 @@ class PangeaTextController extends TextEditingController { text ??= ''; this.text = text; } + + static const int maxLength = 1000; + bool get isMaxLength => text.length == 1000; + bool forceKeepOpen = false; setSystemText(String text, EditType type) { diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart index 655fea198..b5665c635 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:universal_html/html.dart' as html; @@ -80,6 +81,9 @@ Future _constructDatabase(Client client) async { } final cipher = await getDatabaseCipher(); + // #Pangea + Sentry.addBreadcrumb(Breadcrumb(message: 'Database cipher: $cipher')); + // Pangea# Directory? fileStorageLocation; try { @@ -97,6 +101,9 @@ Future _constructDatabase(Client client) async { // import the SQLite / SQLCipher shared objects / dynamic libraries final factory = createDatabaseFactoryFfi(ffiInit: SQfLiteEncryptionHelper.ffiInit); + // #Pangea + Sentry.addBreadcrumb(Breadcrumb(message: 'Database path: $path')); + // Pangea# // migrate from potential previous SQLite database path to current one await _migrateLegacyLocation(path, client.clientName); @@ -113,6 +120,9 @@ Future _constructDatabase(Client client) async { path: path, cipher: cipher, ); + // #Pangea + Sentry.addBreadcrumb(Breadcrumb(message: 'Database cipher helper: $helper')); + // Pangea# // check whether the DB is already encrypted and otherwise do so await helper?.ensureDatabaseFileEncrypted(); diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart index 612f74395..bfe1251dc 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/config/setting_keys.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; const _passwordStorageKey = 'database_password'; @@ -58,6 +59,12 @@ void _sendNoEncryptionWarning(Object exception) async { // l10n.noDatabaseEncryption, // exception.toString(), // ); + Sentry.addBreadcrumb( + Breadcrumb( + message: 'No database encryption', + data: {'exception': exception}, + ), + ); // Pangea# await store.setBool(SettingKeys.noEncryptionWarningShown, true);