Merge branch 'main' of https://github.com/pangeachat/client into move-chat-buttons
This commit is contained in:
commit
276f3ee913
24 changed files with 454 additions and 210 deletions
|
|
@ -4111,5 +4111,7 @@
|
|||
"deleteSubscriptionWarningBody": "Deleting your account will not automatically cancel your subscription.",
|
||||
"manageSubscription": "Manage Subscription",
|
||||
"createSpace": "Create space",
|
||||
"createChat": "Create chat"
|
||||
"createChat": "Create chat",
|
||||
"error520Title": "Please try again.",
|
||||
"error520Desc": "Sorry, we could not understand your message..."
|
||||
}
|
||||
|
|
@ -298,7 +298,10 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
final statusText = this.statusText ??= _durationString ?? '00:00';
|
||||
final audioPlayer = this.audioPlayer;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
// #Pangea
|
||||
// padding: const EdgeInsets.all(12.0),
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
// Pangea#
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
|
|
@ -332,7 +335,10 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// #Pangea
|
||||
// const SizedBox(width: 8),
|
||||
const SizedBox(width: 5),
|
||||
// Pangea#
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
@ -368,7 +374,10 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// #Pangea
|
||||
// const SizedBox(width: 8),
|
||||
const SizedBox(width: 5),
|
||||
// Pangea#
|
||||
SizedBox(
|
||||
width: 36,
|
||||
child: Text(
|
||||
|
|
|
|||
|
|
@ -314,8 +314,9 @@ class Message extends StatelessWidget {
|
|||
padding: const EdgeInsets.only(left: 8),
|
||||
child: GestureDetector(
|
||||
// #Pangea
|
||||
onTap: () =>
|
||||
toolbarController?.showToolbar(context),
|
||||
onTap: () => toolbarController?.showToolbar(
|
||||
context,
|
||||
),
|
||||
onDoubleTap: () =>
|
||||
toolbarController?.showToolbar(context),
|
||||
// Pangea#
|
||||
|
|
@ -585,7 +586,9 @@ class Message extends StatelessWidget {
|
|||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
if (pangeaMessageEvent?.showMessageButtons ?? false)
|
||||
MessageButtons(toolbarController: toolbarController),
|
||||
MessageButtons(
|
||||
toolbarController: toolbarController,
|
||||
),
|
||||
MessageReactions(event, timeline),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/blur_hash.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../../utils/error_reporter.dart';
|
||||
|
||||
class EventVideoPlayer extends StatefulWidget {
|
||||
|
|
@ -71,7 +70,7 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||
autoInitialize: true,
|
||||
);
|
||||
}
|
||||
} on MatrixConnectionException catch (e) {
|
||||
} on Exception catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e.toLocalizedString(context)),
|
||||
|
|
|
|||
|
|
@ -599,12 +599,23 @@ class ChatListController extends State<ChatList>
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
final StreamController<String> selectionsStream =
|
||||
StreamController.broadcast();
|
||||
// Pangea#
|
||||
|
||||
void toggleSelection(String roomId) {
|
||||
setState(
|
||||
() => selectedRoomIds.contains(roomId)
|
||||
? selectedRoomIds.remove(roomId)
|
||||
: selectedRoomIds.add(roomId),
|
||||
);
|
||||
// #Pangea
|
||||
// setState(
|
||||
// () => selectedRoomIds.contains(roomId)
|
||||
// ? selectedRoomIds.remove(roomId)
|
||||
// : selectedRoomIds.add(roomId),
|
||||
// );
|
||||
selectedRoomIds.contains(roomId)
|
||||
? selectedRoomIds.remove(roomId)
|
||||
: selectedRoomIds.add(roomId);
|
||||
selectionsStream.add(roomId);
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
Future<void> toggleUnread() async {
|
||||
|
|
@ -676,8 +687,8 @@ class ChatListController extends State<ChatList>
|
|||
context: context,
|
||||
future: () => _archiveSelectedRooms(),
|
||||
);
|
||||
setState(() {});
|
||||
// #Pangea
|
||||
// setState(() {});
|
||||
if (archivedActiveRoom) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
|
|
@ -709,7 +720,6 @@ class ChatListController extends State<ChatList>
|
|||
context: context,
|
||||
future: () => _leaveSelectedRooms(onlyAdmin),
|
||||
);
|
||||
setState(() {});
|
||||
if (leftActiveRoom) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
|
|
@ -832,8 +842,7 @@ class ChatListController extends State<ChatList>
|
|||
label: space.nameIncludingParents(context),
|
||||
// If user is not admin of space, button is grayed out
|
||||
textStyle: TextStyle(
|
||||
color: (firstSelectedRoom == null ||
|
||||
(firstSelectedRoom.isSpace && !space.isRoomAdmin))
|
||||
color: (firstSelectedRoom == null)
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.surfaceTint,
|
||||
),
|
||||
|
|
@ -851,10 +860,6 @@ class ChatListController extends State<ChatList>
|
|||
if (firstSelectedRoom == null) {
|
||||
throw L10n.of(context)!.nonexistentSelection;
|
||||
}
|
||||
// If user is not admin of the would-be parent space, does not allow
|
||||
if (firstSelectedRoom.isSpace && !space.isRoomAdmin) {
|
||||
throw L10n.of(context)!.cantAddSpaceChild;
|
||||
}
|
||||
|
||||
if (space.canSendDefaultStates) {
|
||||
for (final roomId in selectedRoomIds) {
|
||||
|
|
@ -876,7 +881,12 @@ class ChatListController extends State<ChatList>
|
|||
);
|
||||
}
|
||||
|
||||
setState(() => selectedRoomIds.clear());
|
||||
// #Pangea
|
||||
// setState(() => selectedRoomIds.clear());
|
||||
if (firstSelectedRoom != null) {
|
||||
toggleSelection(firstSelectedRoom.id);
|
||||
}
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any(
|
||||
|
|
@ -946,7 +956,12 @@ class ChatListController extends State<ChatList>
|
|||
if (selectMode == SelectMode.share) {
|
||||
setState(() => Matrix.of(context).shareContent = null);
|
||||
} else {
|
||||
setState(() => selectedRoomIds.clear());
|
||||
// #Pangea
|
||||
// setState(() => selectedRoomIds.clear());
|
||||
for (final roomId in selectedRoomIds.toList()) {
|
||||
toggleSelection(roomId);
|
||||
}
|
||||
// Pangea#
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:animations/animations.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/pages/chat_list/space_view.dart';
|
||||
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_body_text.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
|
@ -17,7 +17,6 @@ import 'package:matrix/matrix.dart';
|
|||
import '../../config/themes.dart';
|
||||
import '../../widgets/connection_status_header.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'chat_list_header.dart';
|
||||
|
||||
class ChatListViewBody extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
|
|
@ -76,7 +75,10 @@ class ChatListViewBody extends StatelessWidget {
|
|||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
slivers: [
|
||||
ChatListHeader(controller: controller),
|
||||
// #Pangea
|
||||
// ChatListHeader(controller: controller),
|
||||
ChatListHeaderWrapper(controller: controller),
|
||||
// Pangea#
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
|
|
@ -247,17 +249,23 @@ class ChatListViewBody extends StatelessWidget {
|
|||
SliverList.builder(
|
||||
itemCount: rooms.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
return ChatListItem(
|
||||
// #Pangea
|
||||
// return ChatListItem(
|
||||
return ChatListItemWrapper(
|
||||
controller: controller,
|
||||
// Pangea#
|
||||
rooms[i],
|
||||
key: Key('chat_list_item_${rooms[i].id}'),
|
||||
filter: filter,
|
||||
selected:
|
||||
controller.selectedRoomIds.contains(rooms[i].id),
|
||||
onTap: controller.selectMode == SelectMode.select
|
||||
? () => controller.toggleSelection(rooms[i].id)
|
||||
: () => onChatTap(rooms[i], context),
|
||||
onLongPress: () =>
|
||||
controller.toggleSelection(rooms[i].id),
|
||||
// #Pangea
|
||||
// selected:
|
||||
// controller.selectedRoomIds.contains(rooms[i].id),
|
||||
// onTap: controller.selectMode == SelectMode.select
|
||||
// ? () => controller.toggleSelection(rooms[i].id)
|
||||
// : () => onChatTap(rooms[i], context),
|
||||
// onLongPress: () =>
|
||||
// controller.toggleSelection(rooms[i].id),
|
||||
// Pangea#
|
||||
activeChat: controller.activeChat == rooms[i].id,
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
|
|
@ -12,6 +11,8 @@ 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/utils/chat_list_handle_space_tap.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -23,7 +24,6 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'chat_list_header.dart';
|
||||
|
||||
class SpaceView extends StatefulWidget {
|
||||
final ChatListController controller;
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -709,7 +725,10 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
child: CustomScrollView(
|
||||
controller: widget.scrollController,
|
||||
slivers: [
|
||||
ChatListHeader(controller: widget.controller),
|
||||
// #Pangea
|
||||
// ChatListHeader(controller: widget.controller),
|
||||
ChatListHeaderWrapper(controller: widget.controller),
|
||||
// Pangea#
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, i) {
|
||||
|
|
@ -789,7 +808,13 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
child: CustomScrollView(
|
||||
controller: widget.scrollController,
|
||||
slivers: [
|
||||
ChatListHeader(controller: widget.controller, globalSearch: false),
|
||||
// #Pangea
|
||||
// ChatListHeader(controller: widget.controller, globalSearch: false),
|
||||
ChatListHeaderWrapper(
|
||||
controller: widget.controller,
|
||||
globalSearch: false,
|
||||
),
|
||||
// Pangea#
|
||||
SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
primary: false,
|
||||
|
|
@ -911,7 +936,11 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
room.membership != Membership.leave
|
||||
// Pangea#
|
||||
) {
|
||||
return ChatListItem(
|
||||
// #Pangea
|
||||
// return ChatListItem(
|
||||
return ChatListItemWrapper(
|
||||
controller: widget.controller,
|
||||
// Pangea#
|
||||
room,
|
||||
onLongPress: () =>
|
||||
_onSpaceChildContextMenu(spaceChild, room),
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
spaceMode = child?.isSpace ?? spaceMode;
|
||||
|
||||
// get the bool for adding chats to spaces
|
||||
final bool canAddChild = _canIAddSpaceChild(child, spaceMode: spaceMode);
|
||||
final bool canAddChild =
|
||||
(child?.isRoomAdmin ?? true) && canSendEvent(EventTypes.SpaceChild);
|
||||
if (!spaceMode) return canAddChild;
|
||||
|
||||
// if adding space to a space, check if the child is an ancestor
|
||||
|
|
|
|||
|
|
@ -306,10 +306,6 @@ extension PangeaRoom on Room {
|
|||
|
||||
bool get canDelete => _canDelete;
|
||||
|
||||
bool canIAddSpaceChild(Room? room, {bool spaceMode = false}) {
|
||||
return _canIAddSpaceChild(room, spaceMode: spaceMode);
|
||||
}
|
||||
|
||||
bool get canIAddSpaceParents => _canIAddSpaceParents;
|
||||
|
||||
bool pangeaCanSendEvent(String eventType) => _pangeaCanSendEvent(eventType);
|
||||
|
|
|
|||
|
|
@ -54,20 +54,20 @@ extension AnalyticsRoomExtension on Room {
|
|||
return Future.value();
|
||||
}
|
||||
|
||||
if (!canSendEvent(EventTypes.SpaceChild)) return;
|
||||
if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return;
|
||||
if (canIAddSpaceChild(null)) {
|
||||
try {
|
||||
await setSpaceChild(analyticsRoom.id);
|
||||
} catch (err) {
|
||||
debugPrint(
|
||||
"Failed to add analytics room ${analyticsRoom.id} for student to space $id",
|
||||
);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "Failed to add analytics room to space $id",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await setSpaceChild(analyticsRoom.id);
|
||||
} catch (err) {
|
||||
debugPrint(
|
||||
"Failed to add analytics room ${analyticsRoom.id} for student to space $id",
|
||||
);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "Failed to add analytics room to space $id",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,36 +78,10 @@ extension UserPermissionsRoomExtension on Room {
|
|||
|
||||
bool get _canDelete => isSpaceAdmin;
|
||||
|
||||
bool _canIAddSpaceChild(Room? room, {bool spaceMode = false}) {
|
||||
if (!isSpace) {
|
||||
ErrorHandler.logError(
|
||||
m: "should not call canIAddSpaceChildren on non-space room. Room id: $id",
|
||||
data: toJson(),
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
final isSpaceAdmin = isRoomAdmin;
|
||||
final isChildRoomAdmin = room?.isRoomAdmin ?? true;
|
||||
|
||||
// if user is not admin of child room, return false
|
||||
if (!isChildRoomAdmin) return false;
|
||||
|
||||
// if the child room is a space, or will be a space,
|
||||
// then the user must be an admin of the parent space
|
||||
if (room?.isSpace ?? spaceMode) return isSpaceAdmin;
|
||||
|
||||
// otherwise, the user can add the child room to the parent
|
||||
// if they're the admin of the parent or if the parent creation
|
||||
// of group chats
|
||||
return isSpaceAdmin || (pangeaRoomRules?.isCreateRooms ?? false);
|
||||
}
|
||||
|
||||
bool get _canIAddSpaceParents =>
|
||||
_isRoomAdmin || pangeaCanSendEvent(EventTypes.SpaceParent);
|
||||
|
||||
//overriding the default canSendEvent to check power levels
|
||||
// Overriding the default canSendEvent to check power levels
|
||||
bool _pangeaCanSendEvent(String eventType) {
|
||||
final powerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content;
|
||||
if (powerLevelsMap == null) return 0 <= ownPowerLevel;
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@ class ErrorCopy {
|
|||
title = l10n.error502504Title;
|
||||
body = l10n.error502504Desc;
|
||||
break;
|
||||
case 520:
|
||||
title = l10n.error520Title;
|
||||
body = l10n.error520Desc;
|
||||
break;
|
||||
case 404:
|
||||
title = l10n.error404Title;
|
||||
body = l10n.error404Desc;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,10 @@ class ToolbarDisplayController {
|
|||
);
|
||||
}
|
||||
|
||||
void showToolbar(BuildContext context, {MessageMode? mode}) {
|
||||
void showToolbar(
|
||||
BuildContext context, {
|
||||
MessageMode? mode,
|
||||
}) {
|
||||
bool toolbarUp = true;
|
||||
if (highlighted) return;
|
||||
if (controller.selectMode) {
|
||||
|
|
@ -78,8 +81,51 @@ class ToolbarDisplayController {
|
|||
final Size transformTargetSize = (targetRenderBox as RenderBox).size;
|
||||
messageWidth = transformTargetSize.width;
|
||||
final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero);
|
||||
final double screenHeight = MediaQuery.of(context).size.height;
|
||||
toolbarUp = targetOffset.dy >= screenHeight / 2;
|
||||
|
||||
// If there is enough space above, procede as normal
|
||||
// Else if there is enough space below, show toolbar underneath
|
||||
if (targetOffset.dy < 320) {
|
||||
final spaceBeneath = MediaQuery.of(context).size.height -
|
||||
(targetOffset.dy + transformTargetSize.height);
|
||||
if (spaceBeneath >= 320) {
|
||||
toolbarUp = false;
|
||||
}
|
||||
|
||||
// See if it's possible to scroll up to make space
|
||||
else if (controller.scrollController.offset - targetOffset.dy + 320 >=
|
||||
controller.scrollController.position.minScrollExtent &&
|
||||
controller.scrollController.offset - targetOffset.dy + 320 <=
|
||||
controller.scrollController.position.maxScrollExtent) {
|
||||
controller.scrollController.animateTo(
|
||||
controller.scrollController.offset - targetOffset.dy + 320,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
}
|
||||
|
||||
// See if it's possible to scroll down to make space
|
||||
else if (controller.scrollController.offset + spaceBeneath - 320 >=
|
||||
controller.scrollController.position.minScrollExtent &&
|
||||
controller.scrollController.offset + spaceBeneath - 320 <=
|
||||
controller.scrollController.position.maxScrollExtent) {
|
||||
controller.scrollController.animateTo(
|
||||
controller.scrollController.offset + spaceBeneath - 320,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
toolbarUp = false;
|
||||
}
|
||||
|
||||
// If message is too big and can't scroll either way
|
||||
// Scroll up as much as possible, and show toolbar above
|
||||
else {
|
||||
controller.scrollController.animateTo(
|
||||
controller.scrollController.position.minScrollExtent,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Widget overlayMessage = OverlayMessage(
|
||||
|
|
@ -106,7 +152,13 @@ class ToolbarDisplayController {
|
|||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
toolbarUp ? toolbar! : overlayMessage,
|
||||
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,
|
||||
const SizedBox(height: 6),
|
||||
toolbarUp ? overlayMessage : toolbar!,
|
||||
],
|
||||
|
|
@ -367,83 +419,85 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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,
|
||||
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),
|
||||
),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(25),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300,
|
||||
minWidth: 300,
|
||||
maxHeight: 300,
|
||||
),
|
||||
),
|
||||
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),
|
||||
],
|
||||
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,
|
||||
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,
|
||||
),
|
||||
onPressed: () => updateMode(mode),
|
||||
),
|
||||
);
|
||||
}).toList() +
|
||||
[
|
||||
Tooltip(
|
||||
message: L10n.of(context)!.more,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.add_reaction_outlined),
|
||||
onPressed: showMore,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ 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;
|
||||
|
|
@ -133,6 +132,22 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
/// 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
|
||||
bool get showWarning {
|
||||
if (MatrixState.pangeaController.instructions.wereInstructionsTurnedOff(
|
||||
InlineInstructions.l1Translation.toString(),
|
||||
)) return false;
|
||||
|
||||
final bool isWrittenInL1 =
|
||||
l1Code != null && widget.messageEvent.originalSent?.langCode == l1Code;
|
||||
final bool isTextIdentical = selectionTranslation != null &&
|
||||
widget.messageEvent.originalSent?.text == selectionTranslation;
|
||||
|
||||
return isWrittenInL1 || isTextIdentical;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_fetchingRepresentation &&
|
||||
|
|
@ -141,19 +156,6 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
return const CardErrorWidget();
|
||||
}
|
||||
|
||||
// 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(),
|
||||
);
|
||||
|
||||
return Container(
|
||||
child: _fetchingRepresentation
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
|
|
|
|||
47
lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart
Normal file
47
lib/pangea/widgets/chat_list/chat_list_header_wrapper.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_header.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A wrapper around ChatListHeader to allow rebuilding on state changes.
|
||||
/// Prevents having to rebuild the entire ChatList when a single item changes.
|
||||
class ChatListHeaderWrapper extends StatefulWidget {
|
||||
final ChatListController controller;
|
||||
final bool globalSearch;
|
||||
|
||||
const ChatListHeaderWrapper({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.globalSearch = true,
|
||||
});
|
||||
|
||||
@override
|
||||
ChatListHeaderWrapperState createState() => ChatListHeaderWrapperState();
|
||||
}
|
||||
|
||||
class ChatListHeaderWrapperState extends State<ChatListHeaderWrapper> {
|
||||
StreamSubscription? stateSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
stateSub = widget.controller.selectionsStream.stream.listen((roomID) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stateSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChatListHeader(
|
||||
controller: widget.controller,
|
||||
globalSearch: widget.globalSearch,
|
||||
);
|
||||
}
|
||||
}
|
||||
71
lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart
Normal file
71
lib/pangea/widgets/chat_list/chat_list_item_wrapper.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// A wrapper around ChatListItem to allow rebuilding on state changes.
|
||||
/// Prevents having to rebuild the entire ChatList when a single item changes.
|
||||
class ChatListItemWrapper extends StatefulWidget {
|
||||
final Room room;
|
||||
final bool activeChat;
|
||||
final void Function()? onForget;
|
||||
final String? filter;
|
||||
final ChatListController controller;
|
||||
|
||||
final void Function()? onLongPress;
|
||||
final void Function()? onTap;
|
||||
|
||||
const ChatListItemWrapper(
|
||||
this.room, {
|
||||
this.activeChat = false,
|
||||
this.onForget,
|
||||
this.filter,
|
||||
required this.controller,
|
||||
this.onLongPress,
|
||||
this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ChatListItemWrapperState createState() => ChatListItemWrapperState();
|
||||
}
|
||||
|
||||
class ChatListItemWrapperState extends State<ChatListItemWrapper> {
|
||||
StreamSubscription? stateSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
stateSub = widget.controller.selectionsStream.stream.listen((roomID) {
|
||||
if (roomID == widget.room.id) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stateSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChatListItem(
|
||||
widget.room,
|
||||
activeChat: widget.activeChat,
|
||||
selected: widget.controller.selectedRoomIds.contains(widget.room.id),
|
||||
onTap: widget.onTap ??
|
||||
(widget.controller.selectMode == SelectMode.select
|
||||
? () => widget.controller.toggleSelection(widget.room.id)
|
||||
: () => onChatTap(widget.room, context)),
|
||||
onLongPress: widget.onLongPress ??
|
||||
() => widget.controller.toggleSelection(widget.room.id),
|
||||
onForget: widget.onForget,
|
||||
filter: widget.filter,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -76,18 +76,6 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
|
|||
)
|
||||
: null;
|
||||
|
||||
if (widget.activeSpaceId != null) {
|
||||
final activeSpace =
|
||||
Matrix.of(context).client.getRoomById(widget.activeSpaceId!);
|
||||
if (activeSpace != null && activeSpace.canIAddSpaceChild(null)) {
|
||||
parent = activeSpace;
|
||||
} else {
|
||||
ErrorHandler.logError(
|
||||
e: Exception('activeSpaceId ${widget.activeSpaceId} not found'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//sort possibleParents
|
||||
//if possibleParent in parents, put first
|
||||
//use sort but use any instead of contains because contains uses == and we want to compare by id
|
||||
|
|
@ -102,6 +90,20 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
|
|||
});
|
||||
|
||||
isOpen = widget.startOpen;
|
||||
|
||||
if (widget.activeSpaceId != null) {
|
||||
final activeSpace =
|
||||
Matrix.of(context).client.getRoomById(widget.activeSpaceId!);
|
||||
if (activeSpace == null) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception('activeSpaceId ${widget.activeSpaceId} not found'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (activeSpace.canSendEvent(EventTypes.SpaceChild)) {
|
||||
parent = activeSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addSingleSpace(String roomToAddId, Room newParent) async {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/encryption.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -67,9 +66,7 @@ extension LocalizedExceptionExtension on Object {
|
|||
supportedVersions,
|
||||
);
|
||||
}
|
||||
if (this is MatrixConnectionException ||
|
||||
this is SocketException ||
|
||||
this is SyncConnectionException) {
|
||||
if (this is SocketException || this is SyncConnectionException) {
|
||||
return L10n.of(context)!.noConnectionToTheServer;
|
||||
}
|
||||
if (this is String) return toString();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -344,4 +344,10 @@ class MatrixLocals extends MatrixLocalizations {
|
|||
@override
|
||||
String startedKeyVerification(String senderName) =>
|
||||
l10n.startedKeyVerification(senderName);
|
||||
|
||||
@override
|
||||
String invitedBy(String senderName) {
|
||||
// TODO: implement invitedBy
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
pubspec.lock
11
pubspec.lock
|
|
@ -1432,11 +1432,12 @@ packages:
|
|||
matrix:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: matrix
|
||||
sha256: bb6de59d0f69e10bb6893130a967f1ffcbfa3d3ffed3864f0736ce3d968e669c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.29.12"
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: "0a95cd8f3cfac8c9b0b59d6ee7fdbdb159949ca3"
|
||||
url: "https://github.com/pangeachat/matrix-dart-sdk.git"
|
||||
source: git
|
||||
version: "0.30.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -70,7 +70,10 @@ dependencies:
|
|||
keyboard_shortcuts: ^0.1.4
|
||||
latlong2: ^0.9.1
|
||||
linkify: ^5.0.0
|
||||
matrix: ^0.29.12
|
||||
matrix:
|
||||
git:
|
||||
url: https://github.com/pangeachat/matrix-dart-sdk.git # repo
|
||||
ref: main # branch
|
||||
native_imaging: ^0.1.1
|
||||
package_info_plus: ^6.0.0
|
||||
pasteboard: ^0.2.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue