Merge branch 'main' into silence-web-focus

This commit is contained in:
ggurdin 2024-07-25 11:29:22 -04:00 committed by GitHub
commit abca75458f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 296 additions and 212 deletions

View file

@ -289,17 +289,20 @@ class MessageContent extends StatelessWidget {
// #Pangea
// return Linkify(
final messageTextStyle = TextStyle(
overflow: TextOverflow.ellipsis,
color: textColor,
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: event.redacted ? TextDecoration.lineThrough : null,
height: 1.3,
);
if (immersionMode && pangeaMessageEvent != null) {
return PangeaRichText(
style: messageTextStyle,
pangeaMessageEvent: pangeaMessageEvent!,
immersionMode: immersionMode,
toolbarController: toolbarController,
return Flexible(
child: PangeaRichText(
style: messageTextStyle,
pangeaMessageEvent: pangeaMessageEvent!,
immersionMode: immersionMode,
toolbarController: toolbarController,
),
);
} else if (pangeaMessageEvent != null) {
toolbarController?.toolbar?.textSelection.setMessageText(

View file

@ -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,

View file

@ -53,6 +53,25 @@ class _SpaceViewState extends State<SpaceView> {
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<SpaceView> {
// 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();
}

View file

@ -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);
}
}

View file

@ -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.

View file

@ -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();
}
}

View file

@ -44,6 +44,7 @@ class InputBarWrapper extends StatefulWidget {
class InputBarWrapperState extends State<InputBarWrapper> {
StreamSubscription? _choreoSub;
String _currentText = '';
@override
void initState() {
@ -61,6 +62,24 @@ class InputBarWrapperState extends State<InputBarWrapper> {
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<InputBarWrapper> {
focusNode: widget.focusNode,
controller: widget.controller,
decoration: widget.decoration,
onChanged: widget.onChanged,
onChanged: refreshOnChange,
autofocus: widget.autofocus,
textInputAction: widget.textInputAction,
readOnly: widget.readOnly,

View file

@ -419,85 +419,83 @@ class MessageToolbarState extends State<MessageToolbar> {
@override
Widget build(BuildContext context) {
return Flexible(
child: Material(
type: MaterialType.transparency,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border.all(
width: 2,
color: Theme.of(context).colorScheme.primary,
),
borderRadius: const BorderRadius.all(
Radius.circular(25),
),
return Material(
type: MaterialType.transparency,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border.all(
width: 2,
color: Theme.of(context).colorScheme.primary,
),
constraints: const BoxConstraints(
maxWidth: 300,
minWidth: 300,
maxHeight: 300,
borderRadius: const BorderRadius.all(
Radius.circular(25),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SingleChildScrollView(
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: toolbarContent ?? const SizedBox(),
),
SizedBox(height: toolbarContent == null ? 0 : 20),
],
),
),
constraints: const BoxConstraints(
maxWidth: 300,
minWidth: 300,
maxHeight: 300,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SingleChildScrollView(
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: toolbarContent ?? const SizedBox(),
),
SizedBox(height: toolbarContent == null ? 0 : 20),
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: MessageMode.values.map((mode) {
if ([
MessageMode.definition,
MessageMode.textToSpeech,
MessageMode.translation,
].contains(mode) &&
widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox.shrink();
}
if (mode == MessageMode.speechToText &&
!widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox.shrink();
}
return Tooltip(
message: mode.tooltip(context),
child: IconButton(
icon: Icon(mode.icon),
color: mode.iconColor(
widget.pangeaMessageEvent,
currentMode,
context,
),
onPressed: () => updateMode(mode),
),
);
}).toList() +
[
Tooltip(
message: L10n.of(context)!.more,
child: IconButton(
icon: const Icon(Icons.add_reaction_outlined),
onPressed: showMore,
),
Row(
mainAxisSize: MainAxisSize.min,
children: MessageMode.values.map((mode) {
if ([
MessageMode.definition,
MessageMode.textToSpeech,
MessageMode.translation,
].contains(mode) &&
widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox.shrink();
}
if (mode == MessageMode.speechToText &&
!widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox.shrink();
}
return Tooltip(
message: mode.tooltip(context),
child: IconButton(
icon: Icon(mode.icon),
color: mode.iconColor(
widget.pangeaMessageEvent,
currentMode,
context,
),
onPressed: () => updateMode(mode),
),
],
),
],
),
);
}).toList() +
[
Tooltip(
message: L10n.of(context)!.more,
child: IconButton(
icon: const Icon(Icons.add_reaction_outlined),
onPressed: showMore,
),
),
],
),
],
),
),
);

View file

@ -118,81 +118,85 @@ class OverlayMessage extends StatelessWidget {
ownMessage: ownMessage,
);
return Material(
color: noBubble ? Colors.transparent : color,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
return Flexible(
child: Material(
color: noBubble ? Colors.transparent : color,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
padding: noBubble || noPadding
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
constraints: BoxConstraints(
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MessageContent(
event.getDisplayEvent(timeline),
textColor: textColor,
borderRadius: borderRadius,
selected: selected,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
toolbarController: toolbarController,
isOverlay: true,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
) ||
(pangeaMessageEvent.showUseType))
Padding(
padding: const EdgeInsets.only(
top: 4.0,
),
padding: noBubble || noPadding
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (pangeaMessageEvent.showUseType) ...[
pangeaMessageEvent.msgUseType.iconView(
context,
textColor.withAlpha(164),
),
const SizedBox(width: 4),
],
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
)) ...[
Icon(
Icons.edit_outlined,
color: textColor.withAlpha(164),
size: 14,
),
Text(
' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}',
style: TextStyle(
color: textColor.withAlpha(164),
fontSize: 12,
),
),
],
],
constraints: BoxConstraints(
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: MessageContent(
event.getDisplayEvent(timeline),
textColor: textColor,
borderRadius: borderRadius,
selected: selected,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
toolbarController: toolbarController,
isOverlay: true,
),
),
],
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
) ||
(pangeaMessageEvent.showUseType))
Padding(
padding: const EdgeInsets.only(
top: 4.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (pangeaMessageEvent.showUseType) ...[
pangeaMessageEvent.msgUseType.iconView(
context,
textColor.withAlpha(164),
),
const SizedBox(width: 4),
],
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
)) ...[
Icon(
Icons.edit_outlined,
color: textColor.withAlpha(164),
size: 14,
),
Text(
' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}',
style: TextStyle(
color: textColor.withAlpha(164),
fontSize: 12,
),
),
],
],
),
),
],
),
),
),
);

View file

@ -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) {

View file

@ -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<MatrixSdkDatabase> _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<MatrixSdkDatabase> _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<MatrixSdkDatabase> _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();

View file

@ -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);