Merge branch 'main' into 512-bump-matrix-sdk

pull/1384/head
ggurdin 1 year ago committed by GitHub
commit e32de242d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -112,6 +112,7 @@ class ChatController extends State<ChatPageWithRoom>
with WidgetsBindingObserver {
// #Pangea
final PangeaController pangeaController = MatrixState.pangeaController;
late Choreographer choreographer = Choreographer(pangeaController, this);
// Pangea#
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
@ -475,10 +476,6 @@ class ChatController extends State<ChatPageWithRoom>
if (kIsWeb && !Matrix.of(context).webHasFocus) return;
// #Pangea
} catch (err, s) {
ErrorHandler.logError(
e: PangeaWarningError("Web focus error: $err"),
s: s,
);
return;
}
// Pangea#

@ -83,6 +83,17 @@ class ChatEmojiPicker extends StatelessWidget {
],
),
),
// #Pangea
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: FloatingActionButton(
onPressed: controller.hideEmojiPicker,
shape: const CircleBorder(),
mini: true,
child: const Icon(Icons.close),
),
),
// Pangea#
],
),
)

@ -3,14 +3,18 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
import 'package:fluffychat/pages/chat/chat_event_list.dart';
import 'package:fluffychat/pages/chat/chat_input_row.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/it_bar.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/widgets/chat/chat_floating_action_button.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/connection_status_header.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -22,10 +26,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import '../../pangea/choreographer/widgets/it_bar.dart';
import '../../utils/stream_extension.dart';
import 'chat_emoji_picker.dart';
import 'chat_input_row.dart';
enum _EventContextAction { info, report }
@ -274,9 +275,6 @@ class ChatView extends StatelessWidget {
// ),
// )
// : null,
floatingActionButton: ChatFloatingActionButton(
controller: controller,
),
// Pangea#
body:
// #Pangea
@ -404,22 +402,32 @@ class ChatView extends StatelessWidget {
),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ITBar(
choreographer:
controller.choreographer,
),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
:
// #Pangea
null,
// Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// const ConnectionStatusHeader(),
// ITBar(
// choreographer:
// controller.choreographer,
// ),
// ReactionsPicker(controller),
// ReplyDisplay(controller),
// ChatInputRow(controller),
// ChatEmojiPicker(controller),
// ],
// ),
// Pangea#
),
),
// #Pangea
// Keep messages above minimum input bar height
SizedBox(
height: (PlatformInfos.isMobile ? 30 : 60),
),
// Pangea#
],
),
),
@ -436,9 +444,69 @@ class ChatView extends StatelessWidget {
// ),
// ),
Positioned(
left: 20,
bottom: 75,
child: StartIGCButton(controller: controller),
left: 0,
right: 0,
bottom: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (!controller.selectMode)
Container(
margin: EdgeInsets.only(
bottom: 10,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.4,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StartIGCButton(
controller: controller,
),
ChatFloatingActionButton(
controller: 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
.surfaceContainerHighest,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
child: Column(
children: [
const ConnectionStatusHeader(),
ITBar(
choreographer: controller.choreographer,
),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
),
),
],
),
),
// Pangea#
],

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

@ -1,9 +1,8 @@
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import '../../../config/app_config.dart';
class ReplyContent extends StatelessWidget {

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

@ -1,9 +1,8 @@
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import '../../config/themes.dart';
import 'chat.dart';
import 'events/reply_content.dart';

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

@ -58,13 +58,10 @@ class LanguagePermissionsButtons extends StatelessWidget {
),
);
return Padding(
padding: const EdgeInsets.only(bottom: 56.0),
child: FloatingActionButton(
mini: true,
child: const Icon(Icons.history_edu_outlined),
onPressed: () => showMessage(context, text),
),
return FloatingActionButton(
mini: true,
child: const Icon(Icons.history_edu_outlined),
onPressed: () => showMessage(context, text),
);
}

@ -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 classContext?.pangeaRoomRules == null
? isUser18()
: classContext!.pangeaRoomRules!.isCreateRooms ||
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!.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);
}
}

@ -32,6 +32,9 @@ enum ConstructUseTypeEnum {
/// selected correctly in practice activity flow
corPA,
/// encountered as distractor in practice activity flow and correctly ignored it
ignPA,
/// was target construct in practice activity but user did not select correctly
incPA,
}
@ -61,6 +64,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
return 'corPA';
case ConstructUseTypeEnum.incPA:
return 'incPA';
case ConstructUseTypeEnum.ignPA:
return 'ignPA';
}
}
@ -71,11 +76,11 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
case ConstructUseTypeEnum.wa:
return Icons.thumb_up_sharp;
case ConstructUseTypeEnum.corIt:
return Icons.check;
return Icons.translate;
case ConstructUseTypeEnum.incIt:
return Icons.close;
return Icons.translate;
case ConstructUseTypeEnum.ignIt:
return Icons.close;
return Icons.translate;
case ConstructUseTypeEnum.ignIGC:
return Icons.close;
case ConstructUseTypeEnum.corIGC:
@ -86,8 +91,45 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
return Icons.check;
case ConstructUseTypeEnum.incPA:
return Icons.close;
case ConstructUseTypeEnum.ignPA:
return Icons.close;
case ConstructUseTypeEnum.unk:
return Icons.help;
}
}
/// Returns the point value for the construct use type
/// This is used to calculate the both the total points for a user and per construct
/// Users get slightly negative points for incorrect uses to encourage them to be more careful
/// They get the most points for direct uses without help.
/// They get a small amount of points for correct uses in interactions.
/// Practice activities get a moderate amount of points.
int get pointValue {
switch (this) {
case ConstructUseTypeEnum.ga:
return 2;
case ConstructUseTypeEnum.wa:
return 3;
case ConstructUseTypeEnum.corIt:
return 1;
case ConstructUseTypeEnum.incIt:
return -1;
case ConstructUseTypeEnum.ignIt:
return 1;
case ConstructUseTypeEnum.ignIGC:
return 1;
case ConstructUseTypeEnum.corIGC:
return 2;
case ConstructUseTypeEnum.incIGC:
return -1;
case ConstructUseTypeEnum.unk:
return 0;
case ConstructUseTypeEnum.corPA:
return 2;
case ConstructUseTypeEnum.incPA:
return -1;
case ConstructUseTypeEnum.ignPA:
return 1;
}
}
}

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:adaptive_dialog/adaptive_dialog.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.
@ -200,30 +203,66 @@ extension AnalyticsRoomExtension on Room {
creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
}
Future<String?> sendSummaryAnalyticsEvent(
Future<void> sendSummaryAnalyticsEvent(
List<RecentMessageRecord> records,
) async {
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
messages: records,
);
final String? eventId = await sendEvent(
await sendEvent(
analyticsModel.toJson(),
type: PangeaEventTypes.summaryAnalytics,
);
return eventId;
}
Future<String?> sendConstructsEvent(
/// Sends construct events to the server.
///
/// The [uses] parameter is a list of [OneConstructUse] objects representing the
/// constructs to be sent. To prevent hitting the maximum event size, the events
/// are chunked into smaller lists. Each chunk is sent as a separate event.
Future<void> sendConstructsEvent(
List<OneConstructUse> uses,
) async {
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
uses: uses,
);
// these events can get big, so we chunk them to prevent hitting the max event size.
// go through each of the uses being sent and add them to the current chunk until
// the size (in bytes) of the current chunk is greater than the max event size, then
// start a new chunk until all uses have been added.
final List<List<OneConstructUse>> useChunks = [];
List<OneConstructUse> currentChunk = [];
int currentChunkSize = 0;
final String? eventId = await sendEvent(
constructsModel.toJson(),
type: PangeaEventTypes.construct,
);
return eventId;
for (final use in uses) {
// get the size, in bytes, of the json representation of the use
final json = use.toJson();
final jsonString = jsonEncode(json);
final jsonSizeInBytes = utf8.encode(jsonString).length;
// If this use would tip this chunk over the size limit,
// add it to the list of all chunks and start a new chunk.
//
// I tested with using the maxPDUSize constant, but the events
// were still too large. 50000 seems to be a safe number of bytes.
if (currentChunkSize + jsonSizeInBytes > (maxPDUSize - 10000)) {
useChunks.add(currentChunk);
currentChunk = [];
currentChunkSize = 0;
}
// add this use to the current chunk
currentChunk.add(use);
currentChunkSize += jsonSizeInBytes;
}
if (currentChunk.isNotEmpty) {
useChunks.add(currentChunk);
}
for (final chunk in useChunks) {
final constructsModel = ConstructAnalyticsModel(uses: chunk);
await sendEvent(
constructsModel.toJson(),
type: PangeaEventTypes.construct,
);
}
}
}

@ -187,6 +187,8 @@ class VocabTotals {
break;
case ConstructUseTypeEnum.unk:
break;
case ConstructUseTypeEnum.ignPA:
break;
}
}
}

@ -13,36 +13,40 @@ class AnalyticsViewButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton<BarChartViewSelection>(
tooltip: L10n.of(context)!.changeAnalyticsView,
initialValue: value,
onSelected: (BarChartViewSelection? view) {
if (view == null) {
debugPrint("when is view null?");
return;
}
onChange(view);
},
itemBuilder: (BuildContext context) => BarChartViewSelection.values
.map<PopupMenuEntry<BarChartViewSelection>>(
(BarChartViewSelection view) {
return PopupMenuItem<BarChartViewSelection>(
value: view,
child: Text(view.string(context)),
);
}).toList(),
child: TextButton.icon(
label: Text(
value.string(context),
style: TextStyle(
return Flexible(
child: PopupMenuButton<BarChartViewSelection>(
tooltip: L10n.of(context)!.changeAnalyticsView,
initialValue: value,
onSelected: (BarChartViewSelection? view) {
if (view == null) {
debugPrint("when is view null?");
return;
}
onChange(view);
},
itemBuilder: (BuildContext context) => BarChartViewSelection.values
.map<PopupMenuEntry<BarChartViewSelection>>(
(BarChartViewSelection view) {
return PopupMenuItem<BarChartViewSelection>(
value: view,
child: Text(
view.string(context),
),
);
}).toList(),
child: TextButton.icon(
label: Text(
value.string(context),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
icon: Icon(
value.icon,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: null,
),
icon: Icon(
value.icon,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: null,
),
);
}

@ -14,35 +14,37 @@ class TimeSpanMenuButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton<TimeSpan>(
tooltip: L10n.of(context)!.changeDateRange,
initialValue: value,
onSelected: (TimeSpan? timeSpan) {
if (timeSpan == null) {
debugPrint("when is timeSpan null?");
return;
}
onChange(timeSpan);
},
itemBuilder: (BuildContext context) =>
TimeSpan.values.map<PopupMenuEntry<TimeSpan>>((TimeSpan timeSpan) {
return PopupMenuItem<TimeSpan>(
value: timeSpan,
child: Text(timeSpan.string(context)),
);
}).toList(),
child: TextButton.icon(
label: Text(
value.string(context),
style: TextStyle(
return Flexible(
child: PopupMenuButton<TimeSpan>(
tooltip: L10n.of(context)!.changeDateRange,
initialValue: value,
onSelected: (TimeSpan? timeSpan) {
if (timeSpan == null) {
debugPrint("when is timeSpan null?");
return;
}
onChange(timeSpan);
},
itemBuilder: (BuildContext context) =>
TimeSpan.values.map<PopupMenuEntry<TimeSpan>>((TimeSpan timeSpan) {
return PopupMenuItem<TimeSpan>(
value: timeSpan,
child: Text(timeSpan.string(context)),
);
}).toList(),
child: TextButton.icon(
label: Text(
value.string(context),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
icon: Icon(
Icons.calendar_month_outlined,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: null,
),
icon: Icon(
Icons.calendar_month_outlined,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: null,
),
);
}

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

@ -67,14 +67,11 @@ class ChatFloatingActionButtonState extends State<ChatFloatingActionButton> {
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),
),
return FloatingActionButton(
onPressed: widget.controller.scrollDown,
heroTag: null,
mini: true,
child: const Icon(Icons.arrow_downward_outlined),
);
}
if (widget.controller.choreographer.errorService.error != null) {

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

@ -58,10 +58,9 @@ class ToolbarDisplayController {
);
}
void showToolbar(
BuildContext context, {
MessageMode? mode,
}) {
void showToolbar(BuildContext context, {MessageMode? mode}) {
// Close keyboard, if open
FocusManager.instance.primaryFocus?.unfocus();
bool toolbarUp = true;
if (highlighted) return;
if (controller.selectMode) {
@ -87,12 +86,13 @@ class ToolbarDisplayController {
if (targetOffset.dy < 320) {
final spaceBeneath = MediaQuery.of(context).size.height -
(targetOffset.dy + transformTargetSize.height);
if (spaceBeneath >= 320) {
toolbarUp = false;
}
// If toolbar is open, opening toolbar beneath without scrolling can cause issues
// if (spaceBeneath >= 320) {
// toolbarUp = false;
// }
// See if it's possible to scroll up to make space
else if (controller.scrollController.offset - targetOffset.dy + 320 >=
if (controller.scrollController.offset - targetOffset.dy + 320 >=
controller.scrollController.position.minScrollExtent &&
controller.scrollController.offset - targetOffset.dy + 320 <=
controller.scrollController.position.maxScrollExtent) {
@ -152,13 +152,7 @@ class ToolbarDisplayController {
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
toolbarUp
// Column is limited to screen height
// If message portion is too tall, decrease toolbar height
// as necessary to prevent toolbar from acting strange
// Problems may still occur if toolbar height is decreased too much
? toolbar!
: overlayMessage,
toolbarUp ? toolbar! : overlayMessage,
const SizedBox(height: 6),
toolbarUp ? overlayMessage : toolbar!,
],
@ -419,85 +413,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,
),
),
],
),
],
),
),
);

@ -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,
),
),
padding: noBubble || noPadding
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
constraints: BoxConstraints(
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
return Flexible(
child: Material(
color: noBubble ? Colors.transparent : color,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
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(
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),
fontSize: 12,
size: 14,
),
),
Text(
' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}',
style: TextStyle(
color: textColor.withAlpha(164),
fontSize: 12,
),
),
],
],
],
),
),
),
],
],
),
),
),
);

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

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

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

Loading…
Cancel
Save