resolve merge conflict

This commit is contained in:
ggurdin 2025-05-22 13:48:22 -04:00
commit 98539327b6
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
87 changed files with 3635 additions and 2653 deletions

View file

@ -4938,5 +4938,23 @@
"addEnvironmentOverride": "Add environment override",
"defaultOption": "Default",
"deleteChatDesc": "Are you sure you want to delete this chat? It will be deleted for all participants and all messages within the chat will no longer be available for practice or learning analytics.",
"deleteSpaceDesc": "The space and any selected chats and/or subspaces will be deleted for all participants and all messages within the chat will no longer be available for practice or learning analytics. This action cannot be undone."
"deleteSpaceDesc": "The space and any selected chats and/or subspaces will be deleted for all participants and all messages within the chat will no longer be available for practice or learning analytics. This action cannot be undone.",
"chatWithActivities": "Chat with activities",
"findYourPeople": "Find your people",
"launch": "Launch",
"launchActivityToChats": "Launch activity to chats",
"searchChats": "Search chats",
"selectChats": "Select chats",
"selectChatToStart": "Complete! Select a chat to start",
"configureSpace": "Configure space",
"pinMessages": "Pin messages",
"setJoinRules": "Set join rules",
"displayNavigationRail": "Show navigation rail on mobile",
"changeGeneralSettings": "Change general settings",
"inviteOtherUsersToRoom": "Invite other users",
"changeTheNameOfTheSpace": "Change the name of the space",
"changeTheDescription": "Change the description",
"changeThePermissions": "Change the permissions",
"introductions": "Introductions",
"announcements": "Announcements"
}

View file

@ -121,7 +121,10 @@ abstract class AppConfig {
'https://8591d0d863b646feb4f3dda7e5dcab38@o256755.ingest.sentry.io/5243143';
// Pangea#
static bool renderHtml = true;
static bool hideRedactedEvents = false;
// #Pangea
// static bool hideRedactedEvents = false;
static bool hideRedactedEvents = true;
// Pangea#
static bool hideUnknownEvents = true;
static bool hideUnimportantStateEvents = true;
static bool separateChatTypes = false;
@ -131,6 +134,9 @@ abstract class AppConfig {
static bool swipeRightToLeftToReply = true;
static bool? sendOnEnter;
static bool showPresences = true;
// #Pangea
static bool displayNavigationRail = true;
// Pangea#
static bool experimentalVoip = false;
static const bool hideTypingUsernames = false;
static const bool hideAllStateEvents = false;

View file

@ -33,7 +33,6 @@ import 'package:fluffychat/pangea/activity_generator/activity_generator.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
import 'package:fluffychat/pangea/activity_suggestions/suggestions_page.dart';
import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/layouts/bottom_nav_layout.dart';
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
import 'package:fluffychat/pangea/login/pages/login_or_signup_view.dart';
import 'package:fluffychat/pangea/login/pages/signup.dart';
@ -188,32 +187,6 @@ abstract class AppRoutes {
),
],
),
ShellRoute(
pageBuilder: chatListShellRouteBuilder,
routes: [
GoRoute(
path: '/homepage',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const SuggestionsPage(),
),
routes: [
...newRoomRoutes,
GoRoute(
path: '/planner',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const ActivityGenerator(),
),
),
],
),
],
),
// Pangea#
ShellRoute(
// Never use a transition on the shell route. Changing the PageBuilder
@ -236,15 +209,7 @@ abstract class AppRoutes {
),
sideView: child,
)
// #Pangea
// : child,
: FluffyThemes.isColumnMode(context) ||
(state.fullPath?.split("/").reversed.elementAt(1) ==
'rooms' &&
state.pathParameters['roomid'] != null)
? child
: BottomNavLayout(mainView: child),
// Pangea#
: child,
),
routes: [
GoRoute(
@ -378,6 +343,39 @@ abstract class AppRoutes {
),
redirect: loggedOutRedirect,
),
// #Pangea
GoRoute(
path: 'homepage',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const SuggestionsPage(),
),
routes: [
...newRoomRoutes,
GoRoute(
path: '/planner',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const ActivityPlannerPage(),
),
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: '/generator',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const ActivityGenerator(),
),
),
],
),
],
),
// Pangea#
ShellRoute(
pageBuilder: (context, state, child) => defaultPageBuilder(
@ -794,21 +792,14 @@ abstract class AppRoutes {
? TwoColumnLayout(
mainView: ChatList(
activeChat: state.pathParameters['roomid'],
// #Pangea
activeSpaceId: state.uri.queryParameters['spaceId'],
activeFilter: state.uri.queryParameters['filter'],
// Pangea#
displayNavigationRail:
state.path?.startsWith('/rooms/settings') != true,
),
sideView: child,
)
: FluffyThemes.isColumnMode(context) ||
(state.fullPath?.split("/").reversed.elementAt(1) ==
'rooms' &&
state.pathParameters['roomid'] != null)
? child
: BottomNavLayout(mainView: child),
: child,
);
// Pangea#
}

View file

@ -32,6 +32,10 @@ abstract class SettingKeys {
'chat.fluffy.swipeRightToLeftToReply';
static const String experimentalVoip = 'chat.fluffy.experimental_voip';
static const String showPresences = 'chat.fluffy.show_presences';
// #Pangea
static const String displayNavigationRail =
'chat.fluffy.display_navigation_rail';
// Pangea#
}
enum AppSettings<T> {

View file

@ -708,7 +708,9 @@ class ChatController extends State<ChatPageWithRoom>
}
void _onRouteChanged() {
stopMediaStream.add(null);
if (!stopMediaStream.isClosed) {
stopMediaStream.add(null);
}
MatrixState.pAnyState.closeAllOverlays();
}

View file

@ -400,7 +400,12 @@ class ChatView extends StatelessWidget {
// #Pangea
// Keep messages above minimum input bar height
if (!controller.room.isAbandonedDMRoom)
SizedBox(height: controller.inputBarHeight),
AnimatedSize(
duration: const Duration(milliseconds: 200),
child: SizedBox(
height: controller.inputBarHeight,
),
),
// Pangea#
],
),

View file

@ -162,65 +162,68 @@ class HtmlMessage extends StatelessWidget {
String fullHtml,
List<PangeaToken> remainingTokens,
) {
for (final node in element.nodes) {
node.replaceWith(_tokenizeHtml(node, fullHtml, remainingTokens));
}
final regex = RegExp(r'(<[^>]+>)');
if (element is dom.Text) {
// once a text element in reached in the HTML tree, find and
// wrap all the spans with matching tokens until all tokens
// have been wrapped or no more text elements remain
String tokenizedText = element.text;
while (remainingTokens.isNotEmpty) {
final tokenText = remainingTokens.first.text.content;
final matches = regex.allMatches(fullHtml);
final List<String> result = <String>[];
int lastEnd = 0;
int startIndex = tokenizedText.lastIndexOf('</token>');
startIndex = startIndex == -1 ? 0 : startIndex + 8;
final int tokenIndex = tokenizedText.indexOf(
tokenText,
startIndex,
);
// if the token is not found in the text, check if the token exist in the full HTML.
// If not, remove the token and continue. If so, break to move on to the next node in the HTML.
if (tokenIndex == -1) {
final fullHtmlIndex = fullHtml.indexOf(tokenText);
if (fullHtmlIndex == -1) {
remainingTokens.removeAt(0);
continue;
} else {
break;
}
}
final token = remainingTokens.removeAt(0);
final tokenEnd = tokenIndex + tokenText.length;
final before = tokenizedText.substring(0, tokenIndex);
final after = tokenizedText.substring(tokenEnd);
tokenizedText =
"$before<token offset=\"${token.text.offset}\" length=\"${token.text.length}\">$tokenText</token>$after";
for (final match in matches) {
if (match.start > lastEnd) {
result.add(fullHtml.substring(lastEnd, match.start)); // Text before tag
}
final newElement = dom.Element.html('<span>$tokenizedText</span>');
return newElement;
result.add(match.group(0)!); // The tag itself
lastEnd = match.end;
}
return element;
if (lastEnd < fullHtml.length) {
result.add(fullHtml.substring(lastEnd)); // Remaining text after last tag
}
for (final PangeaToken token in tokens ?? []) {
final String tokenText = token.text.content;
final substringIndex = result.indexWhere(
(string) =>
string.contains(tokenText) &&
!(string.startsWith('<') && string.endsWith('>')),
);
if (substringIndex == -1) continue;
final int tokenIndex = result[substringIndex].indexOf(tokenText);
if (tokenIndex == -1) continue;
final int tokenLength = tokenText.characters.length;
final before = result[substringIndex].substring(0, tokenIndex);
final after = result[substringIndex].substring(tokenIndex + tokenLength);
result.replaceRange(substringIndex, substringIndex + 1, [
if (before.isNotEmpty) before,
'<token offset="${token.text.offset}" length="${token.text.length}">$tokenText</token>',
if (after.isNotEmpty) after,
]);
}
return dom.Element.html('<span>${result.join()}</span>');
}
// Pangea#
/// Adding line breaks before block elements.
List<InlineSpan> _renderWithLineBreaks(
dom.NodeList nodes,
BuildContext context, {
// #Pangea
// BuildContext context, {
BuildContext context,
TextStyle textStyle, {
// Pangea#
int depth = 1,
}) {
final onlyElements = nodes.whereType<dom.Element>().toList();
return [
for (var i = 0; i < nodes.length; i++) ...[
// Actually render the node child:
_renderHtml(nodes[i], context, depth: depth + 1),
// #Pangea
// _renderHtml(nodes[i], context, depth: depth + 1),
_renderHtml(nodes[i], context, textStyle, depth: depth + 1),
// Pangea#
// Add linebreaks between blocks:
if (nodes[i] is dom.Element &&
onlyElements.indexOf(nodes[i] as dom.Element) <
@ -237,7 +240,11 @@ class HtmlMessage extends StatelessWidget {
/// Transforms a Node to an InlineSpan.
InlineSpan _renderHtml(
dom.Node node,
BuildContext context, {
// #Pangea
// BuildContext context, {
BuildContext context,
TextStyle textStyle, {
// Pangea#
int depth = 1,
}) {
// We must not render elements nested more than 100 elements deep:
@ -276,9 +283,11 @@ class HtmlMessage extends StatelessWidget {
final renderer = TokenRenderingUtil(
pangeaMessageEvent: pangeaMessageEvent,
readingAssistanceMode: readingAssistanceMode,
existingStyle: AppConfig.messageTextStyle(
pangeaMessageEvent!.event,
textColor,
existingStyle: textStyle.merge(
AppConfig.messageTextStyle(
pangeaMessageEvent!.event,
textColor,
),
),
overlayController: overlayController,
isTransitionAnimation: isTransitionAnimation,
@ -418,6 +427,11 @@ class HtmlMessage extends StatelessWidget {
children: _renderWithLineBreaks(
node.nodes,
context,
// #Pangea
textStyle.merge(
linkStyle.copyWith(height: 1.25),
),
// Pangea#
depth: depth,
),
style: linkStyle,
@ -450,6 +464,9 @@ class HtmlMessage extends StatelessWidget {
..._renderWithLineBreaks(
node.nodes,
context,
// #Pangea
textStyle,
// Pangea#
depth: depth,
),
],
@ -478,6 +495,9 @@ class HtmlMessage extends StatelessWidget {
children: _renderWithLineBreaks(
node.nodes,
context,
// #Pangea
textStyle.copyWith(fontStyle: FontStyle.italic),
// Pangea#
depth: depth,
),
),
@ -576,12 +596,28 @@ class HtmlMessage extends StatelessWidget {
node.localName == 'summary',
)
.map(
(node) => _renderHtml(node, context, depth: depth),
// #Pangea
// (node) => _renderHtml(node, context, depth: depth),
(node) => _renderHtml(
node,
context,
textStyle.merge(
TextStyle(
fontSize: fontSize,
color: textColor,
),
),
depth: depth,
),
// Pangea#
)
else
..._renderWithLineBreaks(
node.nodes,
context,
// #Pangea
textStyle,
// Pangea#
depth: depth,
),
],
@ -614,6 +650,11 @@ class HtmlMessage extends StatelessWidget {
children: _renderWithLineBreaks(
node.nodes,
context,
// #Pangea
textStyle.copyWith(
backgroundColor: obscure ? textColor : null,
),
// Pangea#
depth: depth,
),
),
@ -628,6 +669,36 @@ class HtmlMessage extends StatelessWidget {
);
block:
default:
// #Pangea
final style = switch (node.localName) {
'body' => TextStyle(
fontSize: fontSize,
color: textColor,
),
'a' => linkStyle,
'strong' => const TextStyle(fontWeight: FontWeight.bold),
'em' || 'i' => const TextStyle(fontStyle: FontStyle.italic),
'del' ||
'strikethrough' =>
const TextStyle(decoration: TextDecoration.lineThrough),
'u' => const TextStyle(decoration: TextDecoration.underline),
'h1' => TextStyle(fontSize: fontSize * 1.6, height: 2),
'h2' => TextStyle(fontSize: fontSize * 1.5, height: 2),
'h3' => TextStyle(fontSize: fontSize * 1.4, height: 2),
'h4' => TextStyle(fontSize: fontSize * 1.3, height: 1.75),
'h5' => TextStyle(fontSize: fontSize * 1.2, height: 1.75),
'h6' => TextStyle(fontSize: fontSize * 1.1, height: 1.5),
'span' => TextStyle(
color: node.attributes['color']?.hexToColor ??
node.attributes['data-mx-color']?.hexToColor ??
textColor,
backgroundColor: node.attributes['data-mx-bg-color']?.hexToColor,
),
'sup' => const TextStyle(fontFeatures: [FontFeature.superscripts()]),
'sub' => const TextStyle(fontFeatures: [FontFeature.subscripts()]),
_ => null,
};
// Pangea#
return TextSpan(
style: switch (node.localName) {
'body' => TextStyle(
@ -663,6 +734,9 @@ class HtmlMessage extends StatelessWidget {
children: _renderWithLineBreaks(
node.nodes,
context,
// #Pangea
textStyle.merge(style ?? const TextStyle()),
// Pangea#
depth: depth,
),
);
@ -698,6 +772,12 @@ class HtmlMessage extends StatelessWidget {
parsed,
// Pangea#
context,
// #Pangea
TextStyle(
fontSize: fontSize,
color: textColor,
),
// Pangea#
),
style: TextStyle(
fontSize: fontSize,

View file

@ -11,6 +11,7 @@ import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -142,7 +143,7 @@ class MessageContent extends StatelessWidget {
const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
), () {
controller.choreographer.tts.tryToSpeak(
TtsController.tryToSpeak(
token.text.content,
langCode: pangeaMessageEvent!.messageDisplayLangCode,
);

View file

@ -32,10 +32,16 @@ class ReplyContent extends StatelessWidget {
timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent;
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
final color = theme.brightness == Brightness.dark
? theme.colorScheme.onTertiaryContainer
: ownMessage
// Pangea#
? ownMessage
? theme.colorScheme.tertiaryContainer
: theme.colorScheme.tertiary;
: theme.colorScheme.onTertiaryContainer
: theme.colorScheme.tertiary;
// ? theme.colorScheme.onTertiaryContainer
// : ownMessage
// ? theme.colorScheme.tertiaryContainer
// : theme.colorScheme.tertiary;
// Pangea#
return Material(
color: Colors.transparent,
@ -69,7 +75,9 @@ class ReplyContent extends StatelessWidget {
fontWeight: FontWeight.bold,
// #Pangea
// color: color,
color: theme.colorScheme.onSurface,
color: ownMessage && theme.brightness == Brightness.dark
? theme.colorScheme.tertiaryContainer
: theme.colorScheme.onSurface,
// Pangea#
fontSize: fontSize,
),

View file

@ -12,8 +12,8 @@ import 'package:fluffychat/pages/chat_list/dummy_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/pangea/chat_list/widgets/pangea_chat_list_header.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
@ -201,7 +201,8 @@ class ChatListViewBody extends StatelessWidget {
// #Pangea
// if (spaceDelegateCandidates.isNotEmpty &&
// !controller.widget.displayNavigationRail)
if (!controller.widget.displayNavigationRail)
if (!AppConfig.displayNavigationRail &&
!FluffyThemes.isColumnMode(context))
// Pangea#
ActiveFilter.spaces,
]
@ -384,7 +385,7 @@ class PublicRoomsHorizontalListState extends State<PublicRoomsHorizontalList> {
// Pangea#
avatar: publicRooms[i].avatarUrl,
// #Pangea
onPressed: () => PublicRoomDialog.show(
onPressed: () => PublicRoomBottomSheet.show(
context: context,
roomAlias:
publicRooms[i].canonicalAlias ?? publicRooms[i].roomId,

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pangea/chat_list/widgets/chat_list_view_body_wrapper.dart';
@ -31,8 +32,12 @@ class ChatListView extends StatelessWidget {
},
child: Row(
children: [
if (FluffyThemes.isColumnMode(context) &&
controller.widget.displayNavigationRail) ...[
// #Pangea
// if (FluffyThemes.isColumnMode(context) &&
// controller.widget.displayNavigationRail) ...[
if (FluffyThemes.isColumnMode(context) ||
AppConfig.displayNavigationRail) ...[
// Pangea#
SpacesNavigationRail(
activeSpaceId: controller.activeSpaceId,
onGoToChats: controller.clearActiveSpace,

View file

@ -44,7 +44,14 @@ class NaviRailItem extends StatelessWidget {
bottom: 8,
left: 0,
child: AnimatedContainer(
width: isSelected ? 8 : 0,
// #Pangea
// width: isSelected ? 8 : 0,
width: isSelected
? FluffyThemes.isColumnMode(context)
? 8
: 4
: 0,
// Pangea#
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
decoration: BoxDecoration(

View file

@ -17,12 +17,12 @@ import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart';
import 'package:fluffychat/pangea/spaces/widgets/space_view_leaderboard.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@ -313,7 +313,7 @@ class _SpaceViewState extends State<SpaceView> {
// ?.via,
// ),
// );
final joined = await PublicRoomDialog.show(
final joined = await PublicRoomBottomSheet.show(
context: context,
chunk: item,
via: space?.spaceChildren
@ -527,6 +527,18 @@ class _SpaceViewState extends State<SpaceView> {
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
final displayname =
room?.getLocalizedDisplayname() ?? L10n.of(context).nothingFound;
// #Pangea
final joinedParents = room?.spaceParents
.map((parent) {
final roomId = parent.roomId;
if (roomId == null) return null;
return room.client.getRoomById(roomId);
})
.whereType<Room>()
.toList();
// Pangea#
return Scaffold(
// #Pangea
// appBar: AppBar(
@ -539,14 +551,51 @@ class _SpaceViewState extends State<SpaceView> {
_onSpaceAction(SpaceActions.settings);
},
child: AppBar(
// Pangea#
leading: FluffyThemes.isColumnMode(context)
? null
// leading: FluffyThemes.isColumnMode(context)
// ? null
// : Center(
// child: CloseButton(
// onPressed: widget.onBack,
// ),
// ),
leading: joinedParents?.isEmpty ?? true
? FluffyThemes.isColumnMode(context)
? null
: Center(
child: CloseButton(
onPressed: widget.onBack,
),
)
: Center(
child: CloseButton(
onPressed: widget.onBack,
),
child: joinedParents!.length == 1
? IconButton(
icon: const Icon(Icons.arrow_back_outlined),
onPressed: () =>
widget.toParentSpace(joinedParents.first.id),
)
: PopupMenuButton(
popUpAnimationStyle: AnimationStyle(
duration: const Duration(milliseconds: 0),
),
tooltip: null,
useRootNavigator: true,
icon: const Icon(Icons.arrow_back_outlined),
itemBuilder: (context) {
return [
...joinedParents.mapIndexed((i, room) {
return PopupMenuItem(
value: i,
child: Text(room.getLocalizedDisplayname()),
);
}),
];
},
onSelected: (i) {
widget.toParentSpace(joinedParents[i].id);
},
),
),
// Pangea#
automaticallyImplyLeading: false,
titleSpacing: FluffyThemes.isColumnMode(context) ? null : 0,
title: ListTile(
@ -660,14 +709,16 @@ class _SpaceViewState extends State<SpaceView> {
// Pangea#
.toList();
final joinedParents = room.spaceParents
.map((parent) {
final roomId = parent.roomId;
if (roomId == null) return null;
return room.client.getRoomById(roomId);
})
.whereType<Room>()
.toList();
// #Pangea
// final joinedParents = room.spaceParents
// .map((parent) {
// final roomId = parent.roomId;
// if (roomId == null) return null;
// return room.client.getRoomById(roomId);
// })
// .whereType<Room>()
// .toList();
// Pangea#
final filter = _filterController.text.trim().toLowerCase();
return CustomScrollView(
slivers: [
@ -715,51 +766,51 @@ class _SpaceViewState extends State<SpaceView> {
),
),
),
SliverList.builder(
itemCount: joinedParents.length,
itemBuilder: (context, i) {
final displayname =
joinedParents[i].getLocalizedDisplayname();
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 1,
),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
child: ListTile(
minVerticalPadding: 0,
leading: Icon(
Icons.adaptive.arrow_back_outlined,
size: 16,
),
title: Row(
children: [
Avatar(
mxContent: joinedParents[i].avatar,
name: displayname,
// #Pangea
userId: joinedParents[i].directChatMatrixID,
// Pangea#
size: Avatar.defaultSize / 2,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
),
const SizedBox(width: 8),
Expanded(child: Text(displayname)),
],
),
onTap: () =>
widget.toParentSpace(joinedParents[i].id),
),
),
);
},
),
// #Pangea
// SliverList.builder(
// itemCount: joinedParents.length,
// itemBuilder: (context, i) {
// final displayname =
// joinedParents[i].getLocalizedDisplayname();
// return Padding(
// padding: const EdgeInsets.symmetric(
// horizontal: 8,
// vertical: 1,
// ),
// child: Material(
// borderRadius:
// BorderRadius.circular(AppConfig.borderRadius),
// clipBehavior: Clip.hardEdge,
// child: ListTile(
// minVerticalPadding: 0,
// leading: Icon(
// Icons.adaptive.arrow_back_outlined,
// size: 16,
// ),
// title: Row(
// children: [
// Avatar(
// mxContent: joinedParents[i].avatar,
// name: displayname,
// // #Pangea
// userId: joinedParents[i].directChatMatrixID,
// // Pangea#
// size: Avatar.defaultSize / 2,
// borderRadius: BorderRadius.circular(
// AppConfig.borderRadius / 4,
// ),
// ),
// const SizedBox(width: 8),
// Expanded(child: Text(displayname)),
// ],
// ),
// onTap: () =>
// widget.toParentSpace(joinedParents[i].id),
// ),
// ),
// );
// },
// ),
KnockingUsersIndicator(room: room),
// Pangea#
SliverList.builder(

View file

@ -20,7 +20,10 @@ class ChatPermissionsSettingsView extends StatelessWidget {
return Scaffold(
appBar: AppBar(
leading: const Center(child: BackButton()),
title: Text(L10n.of(context).chatPermissions),
// #Pangea
// title: Text(L10n.of(context).chatPermissions),
title: Text(L10n.of(context).permissions),
// Pangea#
),
body: MaxWidthBody(
child: StreamBuilder(
@ -36,28 +39,11 @@ class ChatPermissionsSettingsView extends StatelessWidget {
final powerLevelsContent = Map<String, Object?>.from(
room.getState(EventTypes.RoomPowerLevels)?.content ?? {},
);
final powerLevels =
Map<String, dynamic>.from(powerLevelsContent) // #Pangea
// ..removeWhere((k, v) => v is! int);
..removeWhere(
(k, v) =>
v is! int ||
k.equals("m.call.invite") ||
k.equals("historical") ||
k.equals("state_default"),
);
// Pangea#
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
..removeWhere((k, v) => v is! int);
final eventsPowerLevels = Map<String, int?>.from(
powerLevelsContent.tryGetMap<String, int?>('events') ?? {},
// #Pangea
)..removeWhere(
(k, v) =>
v is! int ||
k.equals("pangea.usranalytics") ||
k.equals(EventTypes.RoomPowerLevels),
);
// )..removeWhere((k, v) => v is! int);
// Pangea#
)..removeWhere((k, v) => v is! int);
return Column(
children: [
ListTile(
@ -69,7 +55,10 @@ class ChatPermissionsSettingsView extends StatelessWidget {
Divider(color: theme.dividerColor),
ListTile(
title: Text(
L10n.of(context).chatPermissions,
// #Pangea
// L10n.of(context).chatPermissions,
L10n.of(context).permissions,
// Pangea#
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
@ -90,48 +79,57 @@ class ChatPermissionsSettingsView extends StatelessWidget {
newLevel: level,
),
canEdit: room.canChangePowerLevel,
// #Pangea
room: room,
// Pangea#
),
// #Pangea
// Divider(color: theme.dividerColor),
// ListTile(
// title: Text(
// L10n.of(context).notifications,
// style: TextStyle(
// color: theme.colorScheme.primary,
// fontWeight: FontWeight.bold,
// ),
// ),
// ),
// Builder(
// builder: (context) {
// const key = 'rooms';
// final value = powerLevelsContent
// .containsKey('notifications')
// ? powerLevelsContent
// .tryGetMap<String, Object?>('notifications')
// ?.tryGet<int>('rooms') ??
// 0
// : 0;
// return PermissionsListTile(
// permissionKey: key,
// permission: value,
// category: 'notifications',
// canEdit: room.canChangePowerLevel,
// onChanged: (level) => controller.editPowerLevel(
// context,
// key,
// value,
// newLevel: level,
// category: 'notifications',
// ),
// );
// },
// ),
// Pangea#
Divider(color: theme.dividerColor),
ListTile(
title: Text(
L10n.of(context).configureChat,
L10n.of(context).notifications,
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
Builder(
builder: (context) {
const key = 'rooms';
final value = powerLevelsContent
.containsKey('notifications')
? powerLevelsContent
.tryGetMap<String, Object?>('notifications')
?.tryGet<int>('rooms') ??
0
: 0;
return PermissionsListTile(
permissionKey: key,
permission: value,
category: 'notifications',
canEdit: room.canChangePowerLevel,
onChanged: (level) => controller.editPowerLevel(
context,
key,
value,
newLevel: level,
category: 'notifications',
),
// #Pangea
room: room,
// Pangea#
);
},
),
Divider(color: theme.dividerColor),
ListTile(
title: Text(
// #Pangea
// L10n.of(context).configureChat,
room.isSpace
? L10n.of(context).configureSpace
: L10n.of(context).configureChat,
// Pangea#
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
@ -151,6 +149,9 @@ class ChatPermissionsSettingsView extends StatelessWidget {
newLevel: level,
category: 'events',
),
// #Pangea
room: room,
// Pangea#
),
],
),

View file

@ -11,6 +11,9 @@ class PermissionsListTile extends StatelessWidget {
final String? category;
final void Function(int? level)? onChanged;
final bool canEdit;
// #Pangea
final Room room;
// Pangea#
const PermissionsListTile({
super.key,
@ -19,6 +22,9 @@ class PermissionsListTile extends StatelessWidget {
this.category,
required this.onChanged,
required this.canEdit,
// #Pangea
required this.room,
// Pangea#
});
String getLocalizedPowerLevelString(BuildContext context) {
@ -29,15 +35,27 @@ class PermissionsListTile extends StatelessWidget {
case 'events_default':
return L10n.of(context).sendMessages;
case 'state_default':
return L10n.of(context).changeGeneralChatSettings;
// #Pangea
// return L10n.of(context).changeGeneralChatSettings;
return L10n.of(context).changeGeneralSettings;
// Pangea#
case 'ban':
return L10n.of(context).banFromChat;
// #Pangea
// return L10n.of(context).banFromChat;
return L10n.of(context).ban;
// Pangea#
case 'kick':
return L10n.of(context).kickFromChat;
// #Pangea
// return L10n.of(context).kickFromChat;
return L10n.of(context).kick;
// Pangea#
case 'redact':
return L10n.of(context).deleteMessage;
case 'invite':
return L10n.of(context).inviteOtherUsers;
// #Pangea
// return L10n.of(context).inviteOtherUsers;
return L10n.of(context).inviteOtherUsersToRoom;
// Pangea#
}
} else if (category == 'notifications') {
switch (permissionKey) {
@ -49,12 +67,20 @@ class PermissionsListTile extends StatelessWidget {
case EventTypes.RoomName:
// #Pangea
// return L10n.of(context).changeTheNameOfTheGroup;
return L10n.of(context).changeTheNameOfTheChat;
return room.isSpace
? L10n.of(context).changeTheNameOfTheSpace
: L10n.of(context).changeTheNameOfTheChat;
// Pangea#
case EventTypes.RoomTopic:
return L10n.of(context).changeTheDescriptionOfTheGroup;
// #Pangea
// return L10n.of(context).changeTheDescriptionOfTheGroup;
return L10n.of(context).changeTheDescription;
// Pangea#
case EventTypes.RoomPowerLevels:
return L10n.of(context).changeTheChatPermissions;
// #Pangea
// return L10n.of(context).changeTheChatPermissions;
return L10n.of(context).changeThePermissions;
// Pangea#
case EventTypes.HistoryVisibility:
return L10n.of(context).changeTheVisibilityOfChatHistory;
case EventTypes.RoomCanonicalAlias:
@ -70,6 +96,10 @@ class PermissionsListTile extends StatelessWidget {
// #Pangea
case EventTypes.SpaceChild:
return L10n.of(context).spaceChildPermission;
case EventTypes.RoomPinnedEvents:
return L10n.of(context).pinMessages;
case EventTypes.RoomJoinRules:
return L10n.of(context).setJoinRules;
// Pangea#
}
}

View file

@ -7,7 +7,6 @@ import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/new_group/new_group_view.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
@ -48,6 +47,8 @@ class NewGroupController extends State<NewGroup> {
bool requiredCodeToJoin = false;
// bool publicGroup = false;
bool get canSubmit => nameController.text.trim().isNotEmpty;
// Pangea#
bool groupCanBeFound = false;
@ -186,10 +187,8 @@ class NewGroupController extends State<NewGroup> {
);
}
}
// if a timeout happened, don't redirect to the chat
if (error != null) return;
// Pangea#
context.go('/rooms/$roomId/invite?filter=groups');
// Pangea#
}
Future<void> _createSpace() async {
@ -218,6 +217,8 @@ class NewGroupController extends State<NewGroup> {
// context.pop<String>(spaceId);
final spaceId = await Matrix.of(context).client.createPangeaSpace(
name: nameController.text,
introChatName: L10n.of(context).introductions,
announcementsChatName: L10n.of(context).announcements,
visibility:
groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private,
joinRules:
@ -235,8 +236,6 @@ class NewGroupController extends State<NewGroup> {
GoogleAnalytics.createClass(room.name, spaceCode);
}
// if a timeout happened, don't redirect to the space
if (error != null) return;
context.go("/rooms?spaceId=$spaceId");
// Pangea#
}
@ -250,10 +249,11 @@ class NewGroupController extends State<NewGroup> {
focusNode.requestFocus();
return;
}
// Pangea#
if (nameController.text.trim().isEmpty &&
createGroupType == CreateGroupType.space) {
// if (nameController.text.trim().isEmpty &&
// createGroupType == CreateGroupType.space) {
if (!canSubmit) {
// Pangea#
setState(() => error = L10n.of(context).pleaseFillOut);
return;
}
@ -270,23 +270,9 @@ class NewGroupController extends State<NewGroup> {
switch (createGroupType) {
case CreateGroupType.group:
// #Pangea
// await _createGroup();
await _createGroup().timeout(
const Duration(
seconds: AppConfig.roomCreationTimeoutSeconds,
),
);
// Pangea#
await _createGroup();
case CreateGroupType.space:
// #Pangea
// await _createSpace();
await _createSpace().timeout(
const Duration(
seconds: AppConfig.roomCreationTimeoutSeconds,
),
);
// Pangea#
await _createSpace();
}
} catch (e, s) {
sdk.Logs().d('Unable to create group', e, s);

View file

@ -131,9 +131,9 @@ class NewGroupView extends StatelessWidget {
onFieldSubmitted: (value) {
controller.loading ? null : controller.submitAction();
},
validator: (value) => value == null || value.isEmpty
? L10n.of(context).pleaseFillOut
: null,
validator: (value) => controller.canSubmit
? null
: L10n.of(context).pleaseFillOut,
focusNode: controller.focusNode,
// Pangea#
),

View file

@ -41,7 +41,11 @@ class SettingsView extends StatelessWidget {
// Pangea#
return Row(
children: [
if (FluffyThemes.isColumnMode(context)) ...[
// #Pangea
// if (FluffyThemes.isColumnMode(context)) ...[
if (FluffyThemes.isColumnMode(context) ||
AppConfig.displayNavigationRail) ...[
// Pangea#
SpacesNavigationRail(
activeSpaceId: null,
onGoToChats: () => context.go('/rooms'),

View file

@ -359,6 +359,14 @@ class SettingsStyleView extends StatelessWidget {
storeKey: SettingKeys.separateChatTypes,
defaultValue: AppConfig.separateChatTypes,
),
// #Pangea
// SettingsSwitchListTile.adaptive(
// title: L10n.of(context).displayNavigationRail,
// onChanged: (b) => AppConfig.displayNavigationRail = b,
// storeKey: SettingKeys.displayNavigationRail,
// defaultValue: AppConfig.displayNavigationRail,
// ),
// Pangea#
],
),
),

View file

@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/activity_generator/activity_generator.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_card.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/activity_planner/suggestion_form_field.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
@ -53,13 +54,15 @@ class ActivityGeneratorView extends StatelessWidget {
padding: const EdgeInsets.all(16),
itemCount: controller.activities!.length,
itemBuilder: (context, index) {
return ActivityPlanCard(
activity: controller.activities![index],
return ActivityPlannerBuilder(
initialActivity: controller.activities![index],
initialFilename: controller.filename,
room: controller.room,
onEdit: (updatedActivity) =>
controller.onEdit(index, updatedActivity),
onChange: controller.update,
initialImageURL: controller.filename,
builder: (c) {
return ActivityPlanCard(
controller: c,
);
},
);
},
);

View file

@ -6,37 +6,23 @@ import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_room_selection.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/utils/file_selector.dart';
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ActivityPlanCard extends StatefulWidget {
final ActivityPlanModel activity;
final Room? room;
final VoidCallback onChange;
final ValueChanged<ActivityPlanModel> onEdit;
final double maxWidth;
final String? initialImageURL;
final ActivityPlannerBuilderState controller;
const ActivityPlanCard({
super.key,
required this.activity,
required this.room,
required this.onChange,
required this.onEdit,
this.maxWidth = 400,
this.initialImageURL,
required this.controller,
});
@override
@ -44,59 +30,8 @@ class ActivityPlanCard extends StatefulWidget {
}
class ActivityPlanCardState extends State<ActivityPlanCard> {
bool _isEditing = false;
late ActivityPlanModel _tempActivity;
late TextEditingController _titleController;
late TextEditingController _learningObjectiveController;
late TextEditingController _instructionsController;
final TextEditingController _newVocabController = TextEditingController();
final FocusNode _vocabFocusNode = FocusNode();
Uint8List? _avatar;
String? _filename;
String? _imageURL;
@override
void initState() {
super.initState();
_tempActivity = widget.activity;
_titleController = TextEditingController(text: _tempActivity.title);
_learningObjectiveController =
TextEditingController(text: _tempActivity.learningObjective);
_instructionsController =
TextEditingController(text: _tempActivity.instructions);
_filename = widget.initialImageURL?.split("/").last;
_imageURL = widget.activity.imageURL ?? widget.initialImageURL;
}
static const double itemPadding = 12;
@override
void dispose() {
_titleController.dispose();
_learningObjectiveController.dispose();
_instructionsController.dispose();
_newVocabController.dispose();
_vocabFocusNode.dispose();
super.dispose();
}
Future<void> _saveEdits() async {
final updatedActivity = ActivityPlanModel(
req: _tempActivity.req,
title: _titleController.text,
learningObjective: _learningObjectiveController.text,
instructions: _instructionsController.text,
vocab: _tempActivity.vocab,
imageURL: widget.activity.imageURL,
);
widget.onEdit(updatedActivity);
setState(() {
_isEditing = false;
});
}
Future<ActivityPlanModel> _addBookmark(ActivityPlanModel activity) async {
try {
return BookmarkedActivitiesRepo.save(activity);
@ -107,418 +42,350 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
} finally {
if (mounted) {
setState(() {});
widget.onChange();
}
}
}
Future<void> _removeBookmark() async {
try {
BookmarkedActivitiesRepo.remove(widget.activity.bookmarkId);
BookmarkedActivitiesRepo.remove(
widget.controller.updatedActivity.bookmarkId,
);
} catch (e, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: stack, data: widget.activity.toJson());
ErrorHandler.logError(
e: e,
s: stack,
data: widget.controller.updatedActivity.toJson(),
);
} finally {
if (mounted) {
setState(() {});
widget.onChange();
}
}
}
void _addVocab() {
setState(() {
_tempActivity.vocab.add(Vocab(lemma: _newVocabController.text, pos: ''));
_newVocabController.clear();
_vocabFocusNode.requestFocus();
});
}
void _removeVocab(int index) {
setState(() {
_tempActivity.vocab.removeAt(index);
});
}
void selectPhoto() async {
final resp = await selectFiles(
context,
type: FileSelectorType.images,
allowMultiple: false,
);
final photo = resp.singleOrNull;
if (photo == null) return;
final bytes = await photo.readAsBytes();
setState(() {
_avatar = bytes;
_filename = photo.name;
});
final url = await Matrix.of(context).client.uploadContent(
bytes,
filename: photo.name,
);
final updatedActivity = ActivityPlanModel(
req: _tempActivity.req,
title: _tempActivity.title,
learningObjective: _tempActivity.learningObjective,
instructions: _tempActivity.instructions,
vocab: _tempActivity.vocab,
imageURL: url.toString(),
);
widget.onEdit(updatedActivity);
}
Future<void> _setAvatarByImageURL() async {
if (_avatar != null || _imageURL == null) return;
final resp = await http
.get(Uri.parse(_imageURL!))
.timeout(const Duration(seconds: 5));
if (mounted) {
setState(() => _avatar = resp.bodyBytes);
}
}
Future<void> _onLaunch() async {
await _setAvatarByImageURL();
await showFutureLoadingDialog(
if (widget.controller.room != null) {
final resp = await showFutureLoadingDialog(
context: context,
future: widget.controller.launchToRoom,
);
if (!resp.isError) {
context.go("/rooms/${widget.controller.room!.id}");
}
return;
}
return showDialog(
context: context,
future: () async {
String? avatarUrl;
if (_avatar != null) {
final client = Matrix.of(context).client;
final url = await client.uploadContent(
_avatar!,
filename: _filename,
);
avatarUrl = url.toString();
}
if (widget.room != null) {
await widget.room?.sendActivityPlan(
widget.activity,
avatar: _avatar,
filename: _filename,
);
context.go("/rooms/${widget.room?.id}");
return;
}
final client = Matrix.of(context).client;
final roomId = await client.createGroupChat(
preset: CreateRoomPreset.publicChat,
visibility: sdk.Visibility.private,
groupName:
widget.activity.title.isNotEmpty ? widget.activity.title : null,
initialState: [
if (_avatar != null) ...[
StateEvent(
type: EventTypes.RoomAvatar,
stateKey: '',
content: {
"url": avatarUrl,
},
),
],
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: defaultPowerLevels(client.userID!),
builder: (context) {
return FullWidthDialog(
dialogContent: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
),
],
enableEncryption: false,
child: ActivityRoomSelection(
controller: widget.controller,
backButton: IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.close),
),
),
),
maxWidth: 400.0,
maxHeight: 650.0,
);
Room? room = client.getRoomById(roomId);
if (room == null) {
await client.waitForRoomInSync(roomId);
room = client.getRoomById(roomId);
}
if (room == null) return;
await room.sendActivityPlan(
widget.activity,
avatar: _avatar,
filename: _filename,
);
context.go("/rooms/$roomId/invite?filter=groups");
},
);
}
bool get isBookmarked =>
BookmarkedActivitiesRepo.isBookmarked(widget.activity);
bool get _isBookmarked => BookmarkedActivitiesRepo.isBookmarked(
widget.controller.updatedActivity,
);
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxWidth),
constraints: const BoxConstraints(maxWidth: 400),
child: Card(
margin: const EdgeInsets.symmetric(vertical: itemPadding),
child: Column(
children: [
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
child: Form(
key: widget.controller.formKey,
child: Column(
children: [
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
),
clipBehavior: Clip.hardEdge,
alignment: Alignment.center,
child: widget.controller.imageURL != null ||
widget.controller.avatar != null
? ClipRRect(
child: widget.controller.avatar == null
? CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: widget.controller.imageURL!,
placeholder: (context, url) {
return const Center(
child: CircularProgressIndicator(),
);
},
errorWidget: (context, url, error) {
return const Padding(
padding: EdgeInsets.all(28.0),
);
},
)
: Image.memory(
widget.controller.avatar!,
fit: BoxFit.cover,
),
)
: const Padding(
padding: EdgeInsets.all(28.0),
),
),
clipBehavior: Clip.hardEdge,
alignment: Alignment.center,
child: _imageURL != null || _avatar != null
? ClipRRect(
child: _avatar == null
? CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: _imageURL!,
placeholder: (context, url) {
return const Center(
child: CircularProgressIndicator(),
);
},
errorWidget: (context, url, error) {
return const Padding(
padding: EdgeInsets.all(28.0),
);
},
)
: Image.memory(
_avatar!,
fit: BoxFit.cover,
),
)
: const Padding(
padding: EdgeInsets.all(28.0),
if (widget.controller.isEditing)
Positioned(
top: 10.0,
right: 10.0,
child: IconButton(
icon: const Icon(Icons.upload_outlined),
onPressed: widget.controller.selectAvatar,
style: IconButton.styleFrom(
backgroundColor: Colors.black,
),
),
if (_isEditing)
Positioned(
top: 10.0,
right: 10.0,
child: IconButton(
icon: const Icon(Icons.upload_outlined),
onPressed: selectPhoto,
style: IconButton.styleFrom(
backgroundColor: Colors.black,
),
),
),
],
],
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Icon(Icons.event_note_outlined),
const SizedBox(width: itemPadding),
Expanded(
child: _isEditing
? TextField(
controller: _titleController,
decoration: InputDecoration(
labelText: L10n.of(context).activityTitle,
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Icon(Icons.event_note_outlined),
const SizedBox(width: itemPadding),
Expanded(
child: widget.controller.isEditing
? TextField(
controller:
widget.controller.titleController,
decoration: InputDecoration(
labelText: L10n.of(context).activityTitle,
),
maxLines: null,
)
: Text(
widget.controller.updatedActivity.title,
style:
Theme.of(context).textTheme.bodyLarge,
),
maxLines: null,
)
: Text(
widget.activity.title,
style: Theme.of(context).textTheme.bodyLarge,
),
),
if (!_isEditing)
IconButton(
onPressed: isBookmarked
? () => _removeBookmark()
: () => _addBookmark(widget.activity),
icon: Icon(
isBookmarked
? Icons.bookmark
: Icons.bookmark_border,
),
),
],
),
const SizedBox(height: itemPadding),
Row(
children: [
Icon(
Symbols.target,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: itemPadding),
Expanded(
child: _isEditing
? TextField(
controller: _learningObjectiveController,
decoration: InputDecoration(
labelText: l10n.learningObjectiveLabel,
),
maxLines: null,
)
: Text(
widget.activity.learningObjective,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
const SizedBox(height: itemPadding),
Row(
children: [
Icon(
Symbols.steps_rounded,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: itemPadding),
Expanded(
child: _isEditing
? TextField(
controller: _instructionsController,
decoration: InputDecoration(
labelText: l10n.instructions,
),
maxLines: null,
)
: Text(
widget.activity.instructions,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
const SizedBox(height: itemPadding),
if (widget.activity.vocab.isNotEmpty) ...[
if (!widget.controller.isEditing)
IconButton(
onPressed: _isBookmarked
? () => _removeBookmark()
: () => _addBookmark(
widget.controller.updatedActivity,
),
icon: Icon(
_isBookmarked
? Icons.bookmark
: Icons.bookmark_border,
),
),
],
),
const SizedBox(height: itemPadding),
Row(
children: [
Icon(
Symbols.dictionary,
Symbols.target,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: itemPadding),
Expanded(
child: Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: List<Widget>.generate(
_tempActivity.vocab.length, (int index) {
return _isEditing
? Chip(
label: Text(
_tempActivity.vocab[index].lemma,
),
onDeleted: () => _removeVocab(index),
backgroundColor: Colors.transparent,
visualDensity: VisualDensity.compact,
shape: const StadiumBorder(
side: BorderSide(
color: Colors.transparent,
),
),
)
: Chip(
label: Text(
_tempActivity.vocab[index].lemma,
),
backgroundColor: Colors.transparent,
visualDensity: VisualDensity.compact,
shape: const StadiumBorder(
side: BorderSide(
color: Colors.transparent,
),
),
);
}).toList(),
child: widget.controller.isEditing
? TextField(
controller: widget.controller
.learningObjectivesController,
decoration: InputDecoration(
labelText: l10n.learningObjectiveLabel,
),
maxLines: null,
)
: Text(
widget.controller.updatedActivity
.learningObjective,
style:
Theme.of(context).textTheme.bodyMedium,
),
),
],
),
const SizedBox(height: itemPadding),
Row(
children: [
Icon(
Symbols.steps_rounded,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: itemPadding),
Expanded(
child: widget.controller.isEditing
? TextField(
controller: widget
.controller.instructionsController,
decoration: InputDecoration(
labelText: l10n.instructions,
),
maxLines: null,
)
: Text(
widget.controller.updatedActivity
.instructions,
style:
Theme.of(context).textTheme.bodyMedium,
),
),
],
),
const SizedBox(height: itemPadding),
if (widget.controller.vocab.isNotEmpty) ...[
Row(
children: [
Icon(
Symbols.dictionary,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: itemPadding),
Expanded(
child: Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: List<Widget>.generate(
widget.controller.vocab.length,
(int index) {
return widget.controller.isEditing
? Chip(
label: Text(
widget
.controller.vocab[index].lemma,
),
onDeleted: () => widget.controller
.removeVocab(index),
backgroundColor: Colors.transparent,
visualDensity: VisualDensity.compact,
shape: const StadiumBorder(
side: BorderSide(
color: Colors.transparent,
),
),
)
: Chip(
label: Text(
widget
.controller.vocab[index].lemma,
),
backgroundColor: Colors.transparent,
visualDensity: VisualDensity.compact,
shape: const StadiumBorder(
side: BorderSide(
color: Colors.transparent,
),
),
);
}).toList(),
),
),
],
),
],
if (widget.controller.isEditing) ...[
const SizedBox(height: itemPadding),
Padding(
padding: const EdgeInsets.only(top: itemPadding),
child: Row(
children: [
Expanded(
child: TextField(
controller: widget.controller.vocabController,
decoration: InputDecoration(
labelText: l10n.addVocabulary,
),
onSubmitted: (value) {
widget.controller.addVocab();
},
),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: widget.controller.addVocab,
),
],
),
),
],
const SizedBox(height: itemPadding),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Tooltip(
message: !widget.controller.isEditing
? l10n.edit
: l10n.saveChanges,
child: IconButton(
icon: Icon(
!widget.controller.isEditing
? Icons.edit
: Icons.save,
),
onPressed: () => !widget.controller.isEditing
? setState(() {
widget.controller.isEditing = true;
})
: widget.controller.saveEdits(),
isSelected: widget.controller.isEditing,
),
),
if (widget.controller.isEditing)
Tooltip(
message: l10n.cancel,
child: IconButton(
icon: const Icon(Icons.cancel),
onPressed: widget.controller.clearEdits,
),
),
],
),
ElevatedButton.icon(
onPressed:
!widget.controller.isEditing ? _onLaunch : null,
icon: const Icon(Icons.send),
label: Text(l10n.launchActivityButton),
),
],
),
],
if (_isEditing) ...[
const SizedBox(height: itemPadding),
Padding(
padding: const EdgeInsets.only(top: itemPadding),
child: Row(
children: [
Expanded(
child: TextField(
controller: _newVocabController,
focusNode: _vocabFocusNode,
decoration: InputDecoration(
labelText: l10n.addVocabulary,
),
onSubmitted: (value) {
_addVocab();
},
),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: _addVocab,
),
],
),
),
],
const SizedBox(height: itemPadding),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Tooltip(
message:
!_isEditing ? l10n.edit : l10n.saveChanges,
child: IconButton(
icon:
Icon(!_isEditing ? Icons.edit : Icons.save),
onPressed: () => !_isEditing
? setState(() {
_isEditing = true;
})
: _saveEdits(),
isSelected: _isEditing,
),
),
if (_isEditing)
Tooltip(
message: l10n.cancel,
child: IconButton(
icon: const Icon(Icons.cancel),
onPressed: () {
setState(() {
_isEditing = false;
});
},
),
),
],
),
ElevatedButton.icon(
onPressed: !_isEditing ? _onLaunch : null,
icon: const Icon(Icons.send),
label: Text(l10n.launchActivityButton),
),
],
),
],
),
),
),
],
],
),
),
),
),

View file

@ -130,13 +130,6 @@ class ActivityPlanMessage extends StatelessWidget {
AppConfig.borderRadius,
),
),
padding:
event.messageType == MessageTypes.Image
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),

View file

@ -0,0 +1,233 @@
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/utils/client_download_content_extension.dart';
import 'package:fluffychat/utils/file_selector.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ActivityPlannerBuilder extends StatefulWidget {
final ActivityPlanModel initialActivity;
final String? initialFilename;
final Room? room;
final Widget Function(ActivityPlannerBuilderState) builder;
final Future<void> Function(
String,
ActivityPlanModel,
Uint8List?,
String?,
)? onEdit;
const ActivityPlannerBuilder({
super.key,
required this.initialActivity,
this.initialFilename,
this.room,
required this.builder,
this.onEdit,
});
@override
State<ActivityPlannerBuilder> createState() => ActivityPlannerBuilderState();
}
class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
bool isEditing = false;
Uint8List? avatar;
String? imageURL;
String? filename;
final TextEditingController titleController = TextEditingController();
final TextEditingController instructionsController = TextEditingController();
final TextEditingController vocabController = TextEditingController();
final TextEditingController participantsController = TextEditingController();
final TextEditingController learningObjectivesController =
TextEditingController();
final List<Vocab> vocab = [];
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_resetActivity();
}
@override
void dispose() {
titleController.dispose();
learningObjectivesController.dispose();
instructionsController.dispose();
vocabController.dispose();
participantsController.dispose();
super.dispose();
}
Room? get room => widget.room;
ActivityPlanModel get updatedActivity {
final int participants = int.tryParse(participantsController.text.trim()) ??
widget.initialActivity.req.numberOfParticipants;
final updatedReq = widget.initialActivity.req;
updatedReq.numberOfParticipants = participants;
return ActivityPlanModel(
req: updatedReq,
title: titleController.text,
learningObjective: learningObjectivesController.text,
instructions: instructionsController.text,
vocab: vocab,
imageURL: imageURL,
);
}
Future<void> _resetActivity() async {
avatar = null;
filename = null;
imageURL = null;
titleController.text = widget.initialActivity.title;
learningObjectivesController.text =
widget.initialActivity.learningObjective;
instructionsController.text = widget.initialActivity.instructions;
participantsController.text =
widget.initialActivity.req.numberOfParticipants.toString();
vocab.clear();
vocab.addAll(widget.initialActivity.vocab);
imageURL = widget.initialActivity.imageURL;
filename = widget.initialFilename;
await _setAvatarByURL();
if (mounted) setState(() {});
}
void setEditing(bool editting) {
isEditing = editting;
if (mounted) setState(() {});
}
void addVocab() {
vocab.insert(
0,
Vocab(
lemma: vocabController.text.trim(),
pos: "",
),
);
vocabController.clear();
if (mounted) setState(() {});
}
void removeVocab(int index) {
vocab.removeAt(index);
if (mounted) setState(() {});
}
void selectAvatar() async {
final photo = await selectFiles(
context,
type: FileSelectorType.images,
allowMultiple: false,
);
final bytes = await photo.singleOrNull?.readAsBytes();
if (mounted) {
setState(() {
avatar = bytes;
filename = photo.singleOrNull?.name;
});
}
}
Future<void> _setAvatarByURL() async {
if (widget.initialActivity.imageURL == null) return;
try {
if (avatar == null) {
if (widget.initialActivity.imageURL!.startsWith("mxc")) {
final client = Matrix.of(context).client;
final mxcUri = Uri.parse(widget.initialActivity.imageURL!);
final data = await client.downloadMxcCached(mxcUri);
avatar = data;
filename = Uri.encodeComponent(
mxcUri.pathSegments.last,
);
} else {
final Response response =
await http.get(Uri.parse(widget.initialActivity.imageURL!));
avatar = response.bodyBytes;
filename = Uri.encodeComponent(
Uri.parse(widget.initialActivity.imageURL!).pathSegments.last,
);
}
}
} catch (err, s) {
ErrorHandler.logError(
e: err,
s: s,
data: {
"imageURL": widget.initialActivity.imageURL,
},
);
}
}
Future<void> updateImageURL() async {
if (avatar == null) return;
final url = await Matrix.of(context).client.uploadContent(
avatar!,
filename: filename,
);
if (!mounted) return;
setState(() {
imageURL = url.toString();
});
}
Future<void> saveEdits() async {
if (!formKey.currentState!.validate()) return;
await updateImageURL();
setEditing(false);
if (widget.onEdit != null) {
await widget.onEdit!(
widget.initialActivity.bookmarkId,
updatedActivity,
avatar,
filename,
);
}
}
Future<void> clearEdits() async {
await _resetActivity();
if (mounted) {
setState(() {
isEditing = false;
});
}
}
Future<void> launchToRoom() async {
return widget.room?.sendActivityPlan(
updatedActivity,
avatar: avatar,
filename: filename,
avatarURL: imageURL,
);
}
@override
Widget build(BuildContext context) => widget.builder(this);
}

View file

@ -14,8 +14,8 @@ enum PageMode {
}
class ActivityPlannerPage extends StatefulWidget {
final String roomID;
const ActivityPlannerPage({super.key, required this.roomID});
final String? roomID;
const ActivityPlannerPage({super.key, this.roomID});
@override
ActivityPlannerPageState createState() => ActivityPlannerPageState();
@ -23,7 +23,9 @@ class ActivityPlannerPage extends StatefulWidget {
class ActivityPlannerPageState extends State<ActivityPlannerPage> {
PageMode pageMode = PageMode.featuredActivities;
Room? get room => Matrix.of(context).client.getRoomById(widget.roomID);
Room? get room => widget.roomID != null
? Matrix.of(context).client.getRoomById(widget.roomID!)
: null;
void _setPageMode(PageMode? mode) {
if (mode == null) return;

View file

@ -11,11 +11,11 @@ import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
class ActivityPlannerPageAppBar extends StatelessWidget
implements PreferredSizeWidget {
final PageMode pageMode;
final String roomID;
final String? roomID;
const ActivityPlannerPageAppBar({
required this.pageMode,
required this.roomID,
this.roomID,
super.key,
});
@ -68,7 +68,9 @@ class ActivityPlannerPageAppBar extends StatelessWidget
alignment: Alignment.center,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () => context.go('/rooms/$roomID/planner/generator'),
onTap: () => roomID != null
? context.go('/rooms/$roomID/planner/generator')
: context.go("/rooms/homepage/planner/generator"),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,

View file

@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart';
@ -97,11 +98,16 @@ class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
showDialog(
context: context,
builder: (context) {
return ActivitySuggestionDialog(
return ActivityPlannerBuilder(
initialActivity: activity,
buttonText: L10n.of(context).inviteAndLaunch,
room: widget.room,
onEdit: _onEdit,
room: widget.room,
builder: (controller) {
return ActivitySuggestionDialog(
controller: controller,
buttonText: l10n.launch,
);
},
);
},
);

View file

@ -0,0 +1,627 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class ActivityRoomSelection extends StatefulWidget {
final ActivityPlannerBuilderState controller;
final Widget backButton;
const ActivityRoomSelection({
super.key,
required this.controller,
required this.backButton,
});
@override
State<ActivityRoomSelection> createState() => ActivityRoomSelectionState();
}
class ActivityRoomSelectionState extends State<ActivityRoomSelection> {
final TextEditingController searchController = TextEditingController();
final FocusNode searchFocusNode = FocusNode();
bool _loading = false;
bool _complete = false;
bool _hasBotDM = true;
List<Room> _launchableRooms = [];
final List<String> _selectedRooms = [];
@override
void initState() {
super.initState();
_launchableRooms = Matrix.of(context)
.client
.rooms
.where((room) {
return room.canSendDefaultStates &&
!room.isSpace &&
!room.isAnalyticsRoom;
})
.toList()
.sorted((a, b) {
final aIsBotDM = a.directChatMatrixID == BotName.byEnvironment;
final bIsBotDM = b.directChatMatrixID == BotName.byEnvironment;
if (aIsBotDM && !bIsBotDM) return -1;
if (!aIsBotDM && bIsBotDM) return 1;
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
});
_hasBotDM = Matrix.of(context).client.rooms.any((room) {
if (room.isDirectChat &&
room.directChatMatrixID == BotName.byEnvironment) {
return true;
}
if (room.botOptions?.mode == BotMode.directChat) {
return true;
}
return false;
});
}
@override
void dispose() {
searchController.dispose();
searchFocusNode.dispose();
super.dispose();
}
List<Room> get _filteredRooms {
final searchText = searchController.text.toLowerCase();
return _launchableRooms.where((room) {
return room.name.toLowerCase().contains(searchText);
}).toList();
}
void _toggleRoomSelection(String roomId) {
_selectedRooms.contains(roomId)
? _selectedRooms.remove(roomId)
: _selectedRooms.add(roomId);
if (_selectedRooms.contains(roomId)) {
_complete = false;
}
setState(() {});
}
Map<String, Room> get _spaceDelegateCandidates {
final spaces = Matrix.of(context).client.rooms.where((r) => r.isSpace);
final candidates = <String, Room>{};
for (final space in spaces) {
for (final spaceChild in space.spaceChildren) {
final roomId = spaceChild.roomId;
if (roomId == null) continue;
candidates[roomId] = space;
}
}
return candidates;
}
final Map<String, int> _launchStatus = {};
Future<void> _sendActivityPlan(Room room) async {
try {
setState(() => _launchStatus[room.id] = 0);
await room.sendActivityPlan(
widget.controller.updatedActivity,
avatar: widget.controller.avatar,
filename: widget.controller.filename,
avatarURL: widget.controller.imageURL,
);
_launchStatus[room.id] = 1;
} catch (e, s) {
_launchStatus[room.id] = -1;
ErrorHandler.logError(
e: e,
s: s,
data: {
"roomID": room.id,
"activity": widget.controller.updatedActivity.toJson(),
"filename": widget.controller.filename,
"avatarURL": widget.controller.imageURL,
},
);
} finally {
if (mounted) {
setState(() {});
}
}
}
Future<String?> _launchBotDM() async {
try {
setState(() => _launchStatus["placeholder"] = 0);
Uri? avatarUrl;
final imageUrl = widget.controller.imageURL ??
widget.controller.updatedActivity.imageURL;
Uint8List? avatar = widget.controller.avatar;
if (avatar != null) {
avatarUrl = await Matrix.of(context).client.uploadContent(
widget.controller.avatar!,
);
} else if (imageUrl != null) {
final Response response = await http.get(Uri.parse(imageUrl));
avatar = response.bodyBytes;
avatarUrl = await Matrix.of(context).client.uploadContent(
avatar,
);
}
// avatar == null ? null : await client.uploadContent(avatar);
final roomId = await Matrix.of(context).client.createRoom(
name: widget.controller.updatedActivity.title,
invite: [BotName.byEnvironment],
isDirect: true,
preset: CreateRoomPreset.trustedPrivateChat,
initialState: [
BotOptionsModel(mode: BotMode.directChat).toStateEvent,
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: defaultPowerLevels(
Matrix.of(context).client.userID!,
),
),
if (avatar != null && avatarUrl != null)
StateEvent(
type: EventTypes.RoomAvatar,
content: {'url': avatarUrl.toString()},
),
],
);
Room? room = Matrix.of(context).client.getRoomById(roomId);
if (room == null) {
await Matrix.of(context).client.waitForRoomInSync(
roomId,
join: true,
);
room = Matrix.of(context).client.getRoomById(roomId);
if (room == null) {
throw Exception("Room not found");
}
await room.sendActivityPlan(
widget.controller.updatedActivity,
avatar: widget.controller.avatar,
filename: widget.controller.filename,
avatarURL: widget.controller.imageURL,
);
}
_launchStatus["placeholder"] = 1;
return roomId;
} catch (e, s) {
_launchStatus["placeholder"] = -1;
ErrorHandler.logError(
e: e,
s: s,
data: {
"activity": widget.controller.updatedActivity.toJson(),
"filename": widget.controller.filename,
"avatarURL": widget.controller.imageURL,
},
);
} finally {
if (mounted) {
setState(() {});
}
}
return null;
}
Future<void> _launch() async {
setState(() => _loading = true);
try {
final List<Future> futures = [];
for (final roomId in _selectedRooms) {
if (_launchStatus[roomId] == 1) {
continue;
}
final Room? room = _launchableRooms.firstWhereOrNull(
(r) => r.id == roomId,
);
if (room == null) {
if (roomId == 'placeholder') futures.add(_launchBotDM());
} else {
futures.add(_sendActivityPlan(room));
}
}
final resp = await Future.wait(futures);
_complete = true;
if (!mounted) return;
if (_selectedRooms.length == 1 &&
_launchStatus[_selectedRooms.first] == 1) {
if (_selectedRooms.first == 'placeholder' && resp.first != null) {
context.go("/rooms/${resp.first}");
Navigator.of(context).pop();
} else if (_selectedRooms.first != 'placeholder') {
context.go('/rooms/${_selectedRooms.first}');
Navigator.of(context).pop();
}
}
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"activity": widget.controller.updatedActivity.toJson(),
"filename": widget.controller.filename,
"avatarURL": widget.controller.imageURL,
},
);
} finally {
if (mounted) {
setState(() => _loading = false);
}
}
}
String _tooltip(String roomId) {
final status = _launchStatus[roomId];
if (status == 0) {
return "Sending...";
} else if (status == 1) {
return "Go to chat";
} else if (status == -1) {
return "Failed to send";
}
return "";
}
void _onTap(Room room) {
final status = _launchStatus[room.id];
if (status == 0) {
return;
} else if (status == 1) {
context.go('/rooms/${room.id}');
Navigator.of(context).pop();
} else if (status == -1) {
return;
}
debugPrint("Toggling room selection for ${room.id}");
_toggleRoomSelection(room.id);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).selectChats),
leading: widget.backButton,
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
spacing: 16.0,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextField(
controller: searchController,
focusNode: searchFocusNode,
textInputAction: TextInputAction.search,
onChanged: (text) => setState(() {}),
decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintText: L10n.of(context).searchChats,
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
floatingLabelBehavior: FloatingLabelBehavior.never,
suffixIcon: searchController.text.isNotEmpty
? IconButton(
tooltip: L10n.of(context).cancel,
icon: const Icon(Icons.close_outlined),
onPressed: () {
setState(() {
searchController.clear();
searchFocusNode.unfocus();
});
},
color: theme.colorScheme.onPrimaryContainer,
)
: IconButton(
onPressed: () => searchFocusNode.requestFocus(),
icon: Icon(
Icons.search_outlined,
color: theme.colorScheme.onPrimaryContainer,
),
),
),
),
),
Expanded(
child: ListView.builder(
itemCount: _filteredRooms.length + (_hasBotDM ? 0 : 1),
itemBuilder: (context, index) {
if (!_hasBotDM && index == 0) {
return ChatActivityPlaceholder(
activity: widget.controller.updatedActivity,
selected: _selectedRooms.contains("placeholder"),
onTap: () {
_toggleRoomSelection("placeholder");
},
tooltip: _tooltip("placeholder"),
status: _launchStatus["placeholder"],
avatar: widget.controller.avatar,
);
}
if (!_hasBotDM) index--;
final room = _filteredRooms[index];
final displayname = room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
);
final space = _spaceDelegateCandidates[room.id];
return Tooltip(
message: _tooltip(room.id),
child: ListTile(
title: Text(displayname),
leading: SizedBox(
width: Avatar.defaultSize,
height: Avatar.defaultSize,
child: Stack(
children: [
if (space != null)
Positioned(
top: 0,
left: 0,
child: Avatar(
border: BorderSide(
width: 2,
color: theme.colorScheme.surface,
),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
mxContent: space.avatar,
size: Avatar.defaultSize * 0.75,
name: space.getLocalizedDisplayname(),
),
),
Positioned(
bottom: 0,
right: 0,
child: Avatar(
border: space == null
? room.isSpace
? BorderSide(
width: 1,
color: theme.dividerColor,
)
: null
: BorderSide(
width: 2,
color: theme.colorScheme.surface,
),
mxContent: room.avatar,
size: Avatar.defaultSize * 0.75,
name: displayname,
presenceUserId: room.directChatMatrixID,
),
),
],
),
),
trailing: Container(
width: 30.0,
height: 30.0,
alignment: Alignment.center,
child: Builder(
builder: (context) {
final status = _launchStatus[room.id];
if (status == 0) {
return const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator.adaptive(),
);
} else if (status == 1) {
return const Icon(
Icons.check_circle_outline,
color: AppConfig.success,
);
} else if (status == -1) {
return Icon(
Icons.error_outline,
color: theme.colorScheme.error,
);
}
return Checkbox(
value: _selectedRooms.contains(room.id),
onChanged: (_) => _onTap(room),
);
},
),
),
onTap: () => _onTap(room),
),
);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _complete
? Padding(
padding: const EdgeInsets.all(8.0),
child: Text(L10n.of(context).selectChatToStart),
)
: ElevatedButton(
onPressed: _selectedRooms.isNotEmpty ? _launch : null,
style: ElevatedButton.styleFrom(
minimumSize: Size.zero,
padding: const EdgeInsets.all(6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
disabledBackgroundColor: theme.colorScheme.primary,
disabledForegroundColor: theme.colorScheme.onPrimary,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_loading
? const Expanded(
child: SizedBox(
height: 10,
child: LinearProgressIndicator(),
),
)
: Text(
L10n.of(context).launchActivityToChats,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onPrimary,
),
),
],
),
),
),
],
),
),
);
}
}
class ChatActivityPlaceholder extends StatelessWidget {
final ActivityPlanModel activity;
final bool selected;
final VoidCallback onTap;
final String tooltip;
final Uint8List? avatar;
final int? status;
const ChatActivityPlaceholder({
required this.activity,
required this.selected,
required this.onTap,
required this.tooltip,
required this.status,
this.avatar,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
const size = Avatar.defaultSize * 0.75;
return Tooltip(
message: tooltip,
child: ListTile(
title: Text(activity.title),
leading: SizedBox(
width: Avatar.defaultSize,
height: Avatar.defaultSize,
child: SizedBox(
width: size,
height: size,
child: Material(
color: theme.brightness == Brightness.light
? Colors.white
: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(size / 2),
side: BorderSide.none,
),
clipBehavior: Clip.hardEdge,
child: avatar != null
? Image.memory(avatar!)
: activity.imageURL != null
? activity.imageURL!.startsWith('mxc')
? MxcImage(
uri: Uri.parse(activity.imageURL!),
width: size,
height: size,
cacheKey: activity.bookmarkId,
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageUrl: activity.imageURL!,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) =>
const SizedBox(),
fit: BoxFit.cover,
)
: const SizedBox(),
),
),
),
trailing: Container(
width: 30.0,
height: 30.0,
alignment: Alignment.center,
child: Builder(
builder: (context) {
if (status == 0) {
return const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator.adaptive(),
);
} else if (status == 1) {
return const Icon(
Icons.check_circle_outline,
color: AppConfig.success,
);
} else if (status == -1) {
return Icon(
Icons.error_outline,
color: theme.colorScheme.error,
);
}
return Checkbox(
value: selected,
onChanged: (_) => onTap(),
);
},
),
),
onTap: onTap,
),
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:shimmer/shimmer.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_plan_search_repo.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart';
@ -44,7 +45,6 @@ class ActivitySuggestionCarousel extends StatefulWidget {
class ActivitySuggestionCarouselState
extends State<ActivitySuggestionCarousel> {
bool _isOpen = true;
bool _loading = true;
String? _error;
@ -138,7 +138,6 @@ class ActivitySuggestionCarouselState
void _close() {
widget.onActivitySelected(null, null, null);
setState(() => _isOpen = false);
}
void _onClickCard() {
@ -150,13 +149,23 @@ class ActivitySuggestionCarouselState
);
return;
}
showDialog(
context: context,
builder: (context) {
return ActivitySuggestionDialog(
return ActivityPlannerBuilder(
initialActivity: _currentActivity!,
buttonText: L10n.of(context).selectActivity,
onLaunch: widget.onActivitySelected,
builder: (controller) {
return ActivitySuggestionDialog(
controller: controller,
buttonText: L10n.of(context).selectActivity,
onLaunch: () => widget.onActivitySelected(
controller.updatedActivity,
controller.avatar,
controller.filename,
),
);
},
);
},
);
@ -167,164 +176,156 @@ class ActivitySuggestionCarouselState
final theme = Theme.of(context);
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: !_isOpen
? const SizedBox.shrink()
: AnimatedOpacity(
duration: FluffyThemes.animationDuration,
opacity: widget.enabled ? 1.0 : 0.5,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: theme.dividerColor),
borderRadius: BorderRadius.circular(24.0),
),
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 4.0,
),
child: Column(
spacing: 16.0,
child: AnimatedOpacity(
duration: FluffyThemes.animationDuration,
opacity: widget.enabled ? 1.0 : 0.5,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: theme.dividerColor),
borderRadius: BorderRadius.circular(24.0),
),
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 4.0,
),
child: Column(
spacing: 16.0,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
L10n.of(context).newChatActivityTitle,
style: theme.textTheme.titleLarge,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: widget.enabled ? _close : null,
),
],
),
Text(
L10n.of(context).newChatActivityTitle,
style: theme.textTheme.titleLarge,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(L10n.of(context).newChatActivityDesc),
),
Row(
spacing: _isColumnMode ? 16.0 : 4.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MouseRegion(
cursor: _canMoveLeft
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
onTap: _canMoveLeft ? _moveLeft : null,
child: Icon(
Icons.chevron_left_outlined,
size: 32.0,
color: _canMoveLeft ? null : theme.disabledColor,
),
),
),
Container(
constraints:
BoxConstraints(maxHeight: _cardHeight + 12.0),
child: _error != null ||
(_currentActivity == null && !_loading)
? const SizedBox.shrink()
: _loading
? Shimmer.fromColors(
baseColor: theme.colorScheme.primary
.withAlpha(50),
highlightColor: theme.colorScheme.primary
.withAlpha(150),
child: Container(
height: _cardHeight,
width: _cardWidth,
decoration: BoxDecoration(
color: theme
.colorScheme.surfaceContainer,
borderRadius:
BorderRadius.circular(24.0),
),
),
)
: ActivitySuggestionCard(
selected: widget.selectedActivity ==
_currentActivity,
activity: _currentActivity!,
onPressed:
widget.enabled ? _onClickCard : null,
width: _cardWidth,
height: _cardHeight,
image: _currentActivity ==
widget.selectedActivity
? widget.selectedActivityImage
: null,
onChange: () {
if (mounted) setState(() {});
},
),
),
MouseRegion(
cursor: _canMoveRight
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
onTap: _canMoveRight ? _moveRight : null,
child: Icon(
Icons.chevron_right_outlined,
size: 32.0,
color: _canMoveRight ? null : theme.disabledColor,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16.0,
children: _activityItems.mapIndexed((i, activity) {
final selected = activity == _currentActivity;
return InkWell(
enableFeedback: widget.enabled,
borderRadius: BorderRadius.circular(12.0),
onTap: widget.enabled
? () => _setActivityByIndex(i)
: null,
child: ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: selected ? 0.0 : 0.5,
sigmaY: selected ? 0.0 : 0.5,
),
child: Opacity(
opacity: selected ? 1.0 : 0.5,
child: ClipOval(
child: SizedBox.fromSize(
size: const Size.fromRadius(12.0),
child: activity.imageURL != null
? CachedNetworkImage(
imageUrl: activity.imageURL!,
errorWidget: (context, url, error) =>
const SizedBox(),
progressIndicatorBuilder:
(context, url, progress) {
return CircularProgressIndicator(
value: progress.progress,
);
},
)
: CircleAvatar(
backgroundColor:
theme.colorScheme.secondary,
radius: 12.0,
),
),
),
),
),
);
}).toList(),
IconButton(
icon: const Icon(Icons.close),
onPressed: widget.enabled ? _close : null,
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(L10n.of(context).newChatActivityDesc),
),
Row(
spacing: _isColumnMode ? 16.0 : 4.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MouseRegion(
cursor: _canMoveLeft
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
onTap: _canMoveLeft ? _moveLeft : null,
child: Icon(
Icons.chevron_left_outlined,
size: 32.0,
color: _canMoveLeft ? null : theme.disabledColor,
),
),
),
Container(
constraints: BoxConstraints(maxHeight: _cardHeight + 12.0),
child: _error != null ||
(_currentActivity == null && !_loading)
? const SizedBox.shrink()
: _loading
? Shimmer.fromColors(
baseColor:
theme.colorScheme.primary.withAlpha(50),
highlightColor:
theme.colorScheme.primary.withAlpha(150),
child: Container(
height: _cardHeight,
width: _cardWidth,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(24.0),
),
),
)
: ActivitySuggestionCard(
selected:
widget.selectedActivity == _currentActivity,
activity: _currentActivity!,
onPressed: widget.enabled ? _onClickCard : null,
width: _cardWidth,
height: _cardHeight,
image:
_currentActivity == widget.selectedActivity
? widget.selectedActivityImage
: null,
onChange: () {
if (mounted) setState(() {});
},
),
),
MouseRegion(
cursor: _canMoveRight
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
onTap: _canMoveRight ? _moveRight : null,
child: Icon(
Icons.chevron_right_outlined,
size: 32.0,
color: _canMoveRight ? null : theme.disabledColor,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16.0,
children: _activityItems.mapIndexed((i, activity) {
final selected = activity == _currentActivity;
return InkWell(
enableFeedback: widget.enabled,
borderRadius: BorderRadius.circular(12.0),
onTap: widget.enabled ? () => _setActivityByIndex(i) : null,
child: ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: selected ? 0.0 : 0.5,
sigmaY: selected ? 0.0 : 0.5,
),
child: Opacity(
opacity: selected ? 1.0 : 0.5,
child: ClipOval(
child: SizedBox.fromSize(
size: const Size.fromRadius(12.0),
child: activity.imageURL != null
? CachedNetworkImage(
imageUrl: activity.imageURL!,
errorWidget: (context, url, error) =>
const SizedBox(),
progressIndicatorBuilder:
(context, url, progress) {
return CircularProgressIndicator(
value: progress.progress,
);
},
)
: CircleAvatar(
backgroundColor:
theme.colorScheme.secondary,
radius: 12.0,
),
),
),
),
),
);
}).toList(),
),
],
),
),
),
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -10,16 +10,14 @@ import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:shimmer/shimmer.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_plan_search_repo.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart';
import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -135,10 +133,15 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
showDialog(
context: context,
builder: (context) {
return ActivitySuggestionDialog(
return ActivityPlannerBuilder(
initialActivity: activity,
buttonText: L10n.of(context).inviteAndLaunch,
room: widget.room,
builder: (controller) {
return ActivitySuggestionDialog(
controller: controller,
buttonText: L10n.of(context).launch,
);
},
);
},
);
@ -165,7 +168,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
children: [
Flexible(
child: Text(
L10n.of(context).startChat,
L10n.of(context).chatWithActivities,
style: isColumnMode
? theme.textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.bold)
@ -175,91 +178,10 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
overflow: TextOverflow.ellipsis,
),
),
Material(
type: MaterialType.transparency,
child: Row(
spacing: 8.0,
children: [
InkWell(
customBorder: const CircleBorder(),
onTap: () => context.go('/homepage/newgroup'),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(36.0),
),
padding: const EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 10.0,
),
child: Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
CustomizedSvg(
svgUrl:
"${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.plusIconPath}",
colorReplacements: {
"#CDBEF9": colorToHex(
Theme.of(context).colorScheme.secondary,
),
},
height: 16.0,
width: 16.0,
),
Text(
isColumnMode
? L10n.of(context).createOwnChat
: L10n.of(context).chat,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
),
InkWell(
customBorder: const CircleBorder(),
onTap: () => context.go('/homepage/planner'),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(36.0),
),
padding: const EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 10.0,
),
child: Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
CustomizedSvg(
svgUrl:
"${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.crayonIconPath}",
colorReplacements: {
"#CDBEF9": colorToHex(
Theme.of(context).colorScheme.secondary,
),
},
height: 16.0,
width: 16.0,
),
Text(
isColumnMode
? L10n.of(context).makeYourOwnActivity
: L10n.of(context).createActivity,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
IconButton(
icon: const Icon(Icons.menu_outlined),
onPressed: () => context.go('/rooms/homepage/planner'),
tooltip: L10n.of(context).activityPlannerTitle,
),
],
),

View file

@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.dart';
import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart';
import 'package:fluffychat/pangea/public_spaces/public_spaces_area.dart';
import 'package:fluffychat/widgets/navigation_rail.dart';
class SuggestionsPage extends StatelessWidget {
const SuggestionsPage({super.key});
@ -11,25 +15,45 @@ class SuggestionsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isColumnMode = FluffyThemes.isColumnMode(context);
return SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 16.0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 24.0,
children: [
if (!isColumnMode) const LearningProgressIndicators(),
const ActivitySuggestionsArea(
showTitle: true,
scrollDirection: Axis.horizontal,
return Material(
child: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isColumnMode && AppConfig.displayNavigationRail) ...[
SpacesNavigationRail(
activeSpaceId: null,
onGoToChats: () => context.go('/rooms'),
onGoToSpaceId: (spaceId) =>
context.go('/rooms?spaceId=$spaceId'),
),
Container(
color: Theme.of(context).dividerColor,
width: 1,
),
const PublicSpacesArea(),
],
),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 16.0,
),
child: Column(
spacing: 24.0,
children: [
if (!isColumnMode) const LearningProgressIndicators(),
const ActivitySuggestionsArea(
showTitle: true,
scrollDirection: Axis.horizontal,
),
const PublicSpacesArea(),
],
),
),
),
),
],
),
),
);

View file

@ -56,6 +56,7 @@ class VocabDetailsView extends StatelessWidget {
),
iconSize: _iconSize,
uniqueID: "${_construct.lemma}-${_construct.category}",
langCode: _userL2!,
),
subtitle: Column(
children: [
@ -140,8 +141,12 @@ class VocabDetailsView extends StatelessWidget {
children: [
WordTextWithAudioButton(
text: form,
style: Theme.of(context).textTheme.bodyLarge,
style:
Theme.of(context).textTheme.bodyLarge?.copyWith(
color: textColor,
),
uniqueID: "$form-${_construct.lemma}-$i",
langCode: _userL2!,
),
if (i != forms.length - 1) const Text(", "),
],

View file

@ -105,147 +105,156 @@ class LearningProgressIndicatorsState
final mxid = client.userID ?? L10n.of(context).user;
final displayname = _profile?.displayName ?? mxid.localpart ?? mxid;
return Row(
children: [
Tooltip(
message: L10n.of(context).settings,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => context.go("/rooms/settings"),
child: Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
right: 8.0,
),
child: Stack(
clipBehavior: Clip.none, // Allow overflow
children: [
FutureBuilder<Profile>(
future: client.fetchOwnProfile(),
builder: (context, snapshot) => Stack(
alignment: Alignment.center,
children: [
Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(99),
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
client.userID!.localpart,
size: 60,
),
),
],
),
),
Positioned(
bottom: -3,
right: -3,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: Theme.of(context).colorScheme.surfaceBright,
),
padding: const EdgeInsets.all(4.0),
child: Icon(
size: 14,
Icons.settings_outlined,
color: Theme.of(context).colorScheme.primary,
weight: 1000,
),
),
),
],
),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 6.0,
children: [
Text(
displayname,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
LearningSettingsButton(
onTap: () => showDialog(
context: context,
builder: (c) => const SettingsLearning(),
barrierDismissible: false,
),
l2: userL2?.langCode.toUpperCase(),
),
],
),
const SizedBox(height: 6),
Row(
spacing: 6.0,
children: ConstructTypeEnum.values
.map(
(c) => ProgressIndicatorBadge(
points: uniqueLemmas(c.indicator),
loading: _loading,
onTap: () {
showDialog<AnalyticsPopupWrapper>(
context: context,
builder: (context) => AnalyticsPopupWrapper(
view: c,
),
);
},
indicator: c.indicator,
),
)
.toList(),
),
const SizedBox(height: 6),
MouseRegion(
return LayoutBuilder(
builder: (context, constraints) {
return Row(
children: [
Tooltip(
message: L10n.of(context).settings,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
showDialog<LevelBarPopup>(
context: context,
builder: (c) => const LevelBarPopup(),
);
},
child: SizedBox(
height: 26,
onTap: () => context.go("/rooms/settings"),
child: Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
right: 8.0,
),
child: Stack(
alignment: Alignment.center,
clipBehavior: Clip.none, // Allow overflow
children: [
Positioned(
left: 16,
right: 0,
child: LearningProgressBar(
level: _constructsModel.level,
totalXP: _constructsModel.totalXP,
FutureBuilder<Profile>(
future: client.fetchOwnProfile(),
builder: (context, snapshot) => Stack(
alignment: Alignment.center,
children: [
Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(99),
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
client.userID!.localpart,
size: 60,
),
),
],
),
),
Positioned(
left: 0,
child: LevelBadge(level: _constructsModel.level),
bottom: -3,
right: -3,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color:
Theme.of(context).colorScheme.surfaceBright,
),
padding: const EdgeInsets.all(4.0),
child: Icon(
size: 14,
Icons.settings_outlined,
color: Theme.of(context).colorScheme.primary,
weight: 1000,
),
),
),
],
),
),
),
),
],
),
),
],
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 6.0,
children: [
Text(
displayname,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
LearningSettingsButton(
onTap: () => showDialog(
context: context,
builder: (c) => const SettingsLearning(),
barrierDismissible: false,
),
l2: userL2?.langCode.toUpperCase(),
),
],
),
const SizedBox(height: 6),
Row(
spacing: 6.0,
children: ConstructTypeEnum.values
.map(
(c) => ProgressIndicatorBadge(
points: uniqueLemmas(c.indicator),
loading: _loading,
onTap: () {
showDialog<AnalyticsPopupWrapper>(
context: context,
builder: (context) => AnalyticsPopupWrapper(
view: c,
),
);
},
indicator: c.indicator,
mini: constraints.maxWidth < 300,
),
)
.toList(),
),
const SizedBox(height: 6),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
showDialog<LevelBarPopup>(
context: context,
builder: (c) => const LevelBarPopup(),
);
},
child: SizedBox(
height: 26,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
left: 16,
right: 0,
child: LearningProgressBar(
level: _constructsModel.level,
totalXP: _constructsModel.totalXP,
),
),
Positioned(
left: 0,
child: LevelBadge(
level: _constructsModel.level,
mini: constraints.maxWidth < 300,
),
),
],
),
),
),
),
],
),
),
],
);
},
);
}
}

View file

@ -2,14 +2,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_summary/level_bar_popup.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
class LevelBadge extends StatelessWidget {
final int level;
final bool mini;
const LevelBadge({
required this.level,
this.mini = false,
super.key,
});
@ -31,29 +33,13 @@ class LevelBadge extends StatelessWidget {
color: Theme.of(context).colorScheme.surfaceBright,
),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
backgroundColor: AppConfig.gold,
radius: 8,
child: Icon(
size: 12,
Icons.star,
color: Theme.of(context).colorScheme.surfaceBright,
weight: 1000,
),
),
const SizedBox(width: 4),
Text(
L10n.of(context).levelShort(level),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
],
child: Text(
"${mini ? "$level" : L10n.of(context).levelShort(level)}",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
),
);

View file

@ -43,27 +43,13 @@ class LevelBarPopup extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const CircleAvatar(
radius: 20,
backgroundColor: AppConfig.gold,
child: Icon(
size: 30,
Icons.star,
color: Colors.white,
),
),
const SizedBox(width: 10),
Text(
L10n.of(context).levelShort(level),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w900,
color: AppConfig.gold,
),
),
],
Text(
"${L10n.of(context).levelShort(level)}",
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w900,
color: AppConfig.gold,
),
),
Opacity(
opacity: 0.25,

View file

@ -95,14 +95,6 @@ class AnimatedLevelBarState extends State<AnimatedLevelBar>
borderRadius: const BorderRadius.all(
Radius.circular(AppConfig.borderRadius),
),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(50),
spreadRadius: 0,
blurRadius: 5,
offset: const Offset(5, 0),
),
],
),
),
Positioned(

View file

@ -9,6 +9,7 @@ class ProgressIndicatorBadge extends StatelessWidget {
final int points;
final VoidCallback onTap;
final ProgressIndicatorEnum indicator;
final bool mini;
const ProgressIndicatorBadge({
super.key,
@ -16,6 +17,7 @@ class ProgressIndicatorBadge extends StatelessWidget {
required this.indicator,
required this.loading,
required this.points,
this.mini = false,
});
@override
@ -42,15 +44,17 @@ class ProgressIndicatorBadge extends StatelessWidget {
color: indicator.color(context),
weight: 1000,
),
const SizedBox(width: 4.0),
Text(
indicator.tooltip(context),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: indicator.color(context),
if (!mini) ...[
const SizedBox(width: 4.0),
Text(
indicator.tooltip(context),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: indicator.color(context),
),
),
),
],
const SizedBox(width: 4.0),
!loading
? Text(

View file

@ -1,32 +1,60 @@
Map<String, dynamic> defaultPowerLevels(String userID) => {
"ban": 50,
"kick": 50,
"invite": 50,
"redact": 50,
"events": {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.encryption": 100,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100,
"m.room.server_acl": 100,
"m.room.tombstone": 100,
"m.room.pinned_events": 50,
},
"events_default": 0,
"state_default": 50,
"users": {
userID: 100,
},
"users_default": 0,
"notifications": {
"room": 50,
},
};
Map<String, dynamic> restrictedPowerLevels(String userID) => {
"events_default": 50,
"ban": 50,
"kick": 50,
"invite": 50,
"redact": 50,
"events": {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.encryption": 100,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100,
"m.room.server_acl": 100,
"m.room.tombstone": 100,
"m.room.pinned_events": 50,
},
"events_default": 50,
"state_default": 50,
"users": {
userID: 100,
},
"users_default": 0,
"notifications": {
"room": 50,
},
};
Map<String, dynamic> defaultSpacePowerLevels(String userID) => {
"ban": 50,
"kick": 50,
"invite": 50,
"redact": 50,
"events": {
"m.room.power_levels": 100,
"m.room.join_rules": 100,
"m.space.child": 50,
},
"events_default": 0,
"state_default": 50,
"users": {
userID: 100,
},
"users_default": 0,
"notifications": {
"room": 50,
},
};

View file

@ -74,7 +74,8 @@ class ChatFloatingActionButtonState extends State<ChatFloatingActionButton> {
child: const Icon(Icons.arrow_downward_outlined),
);
}
if (widget.controller.choreographer.errorService.error != null) {
if (widget.controller.choreographer.errorService.error != null &&
!widget.controller.choreographer.itController.willOpen) {
return ChoreographerHasErrorButton(
widget.controller.choreographer.errorService.error!,
widget.controller.choreographer,

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
@ -22,17 +24,27 @@ class ChatInputBar extends StatefulWidget {
}
class ChatInputBarState extends State<ChatInputBar> {
void updateHeight() {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox == null || !renderBox.hasSize) return;
widget.controller.updateInputBarHeight(renderBox.size.height);
Timer? _debounceTimer;
void _updateHeight() {
_debounceTimer = Timer(const Duration(milliseconds: 100), () {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox == null || !renderBox.hasSize) return;
widget.controller.updateInputBarHeight(renderBox.size.height);
});
}
@override
void dispose() {
_debounceTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (SizeChangedLayoutNotification notification) {
WidgetsBinding.instance.addPostFrameCallback((_) => updateHeight());
WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight());
return true;
},
child: SizeChangedLayoutNotifier(

View file

@ -342,7 +342,7 @@ class PangeaChatDetailsView extends StatelessWidget {
if (room.isSpace && room.isRoomAdmin && kIsWeb)
DownloadSpaceAnalyticsButton(space: room),
Divider(color: theme.dividerColor, height: 1),
if (room.isRoomAdmin && !room.isSpace)
if (room.ownPowerLevel >= 50 && !room.isSpace)
ListTile(
title: Text(
L10n.of(context).downloadGroupText,
@ -361,7 +361,7 @@ class PangeaChatDetailsView extends StatelessWidget {
),
onTap: () => _downloadChat(context),
),
if (room.isRoomAdmin && !room.isSpace)
if (room.ownPowerLevel >= 50 && !room.isSpace)
Divider(color: theme.dividerColor, height: 1),
if (isGroupChat)
ListTile(

View file

@ -13,7 +13,6 @@ import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart';
import 'package:fluffychat/pangea/choreographer/utils/input_paste_listener.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
@ -21,7 +20,6 @@ import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
@ -43,7 +41,6 @@ class Choreographer {
late ITController itController;
late IgcController igc;
late ErrorService errorService;
late TtsController tts;
bool isFetching = false;
int _timesClicked = 0;
@ -64,7 +61,6 @@ class Choreographer {
_initialize();
}
_initialize() {
tts = TtsController(chatController: chatController);
_textController = PangeaTextController(choreographer: this);
InputPasteListener(_textController, onPaste);
itController = ITController(this);
@ -150,56 +146,57 @@ class Choreographer {
)
: null;
final detectionResp = await LanguageDetectionRepo.get(
MatrixState.pangeaController.userController.accessToken,
request: LanguageDetectionRequest(
PangeaMessageTokens? tokensSent;
PangeaRepresentation? originalSent;
try {
TokensResponseModel? res;
if (l1LangCode != null && l2LangCode != null) {
res = await pangeaController.messageData
.getTokens(
repEventId: null,
room: chatController.room,
req: TokensRequestModel(
fullText: currentText,
senderL1: l1LangCode!,
senderL2: l2LangCode!,
),
)
.timeout(const Duration(seconds: 10));
}
originalSent = PangeaRepresentation(
langCode: res?.detections.firstOrNull?.langCode ??
LanguageKeys.unknownLanguage,
text: currentText,
senderl1: l1LangCode,
senderl2: l2LangCode,
),
);
final detections = detectionResp.detections;
final detectedLanguage =
detections.firstOrNull?.langCode ?? LanguageKeys.unknownLanguage;
final PangeaRepresentation originalSent = PangeaRepresentation(
langCode: detectedLanguage,
text: currentText,
originalSent: true,
originalWritten: originalWritten == null,
);
List<PangeaToken>? res;
if (l1LangCode != null && l2LangCode != null) {
res = await pangeaController.messageData.getTokens(
repEventId: null,
room: chatController.room,
req: TokensRequestModel(
fullText: currentText,
langCode: detectedLanguage,
senderL1: l1LangCode!,
senderL2: l2LangCode!,
),
originalSent: true,
originalWritten: originalWritten == null,
);
tokensSent = res != null
? PangeaMessageTokens(
tokens: res.tokens,
detections: res.detections,
)
: null;
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"currentText": currentText,
"l1LangCode": l1LangCode,
"l2LangCode": l2LangCode,
"choreoRecord": choreoRecord.toJson(),
},
);
} finally {
chatController.send(
originalSent: originalSent,
tokensSent: tokensSent,
choreo: choreoRecord,
);
clear();
}
final PangeaMessageTokens? tokensSent = res != null
? PangeaMessageTokens(
tokens: res,
detections: detections,
)
: null;
chatController.send(
// originalWritten: originalWritten,
originalSent: originalSent,
tokensSent: tokensSent,
//TODO - save originalwritten tokens
// choreo: applicableChoreo,
choreo: choreoRecord,
);
clear();
}
_resetDebounceTimer() {
@ -456,11 +453,6 @@ class Choreographer {
if (!isNormalizationError) continue;
final match = igc.igcTextData!.matches[i];
choreoRecord.addRecord(
_textController.text,
match: match.copyWith..status = PangeaMatchStatus.automatic,
);
igc.igcTextData!.acceptReplacement(
i,
match.match.choices!.indexWhere(
@ -468,6 +460,19 @@ class Choreographer {
),
);
final newMatch = match.copyWith;
newMatch.status = PangeaMatchStatus.automatic;
newMatch.match.length = match.match.choices!
.firstWhere((c) => c.isBestCorrection)
.value
.characters
.length;
choreoRecord.addRecord(
_textController.text,
match: newMatch,
);
_textController.setSystemText(
igc.igcTextData!.originalInput,
EditType.igc,
@ -566,7 +571,7 @@ class Choreographer {
_textController.dispose();
_languageStream?.cancel();
stateStream.close();
tts.dispose();
TtsController.stop();
}
LanguageModel? get l2Lang {

View file

@ -65,7 +65,17 @@ class ErrorService {
return Duration(seconds: coolDownSeconds);
}
final List<String> _errorCache = [];
setError(ChoreoError? error, {Duration? duration}) {
if (_errorCache.contains(error?.raw.toString())) {
return;
}
if (error != null) {
_errorCache.add(error.raw.toString());
}
_error = error;
Future.delayed(duration ?? defaultCooldown, () {
clear();

View file

@ -104,7 +104,9 @@ class IgcController {
}
final IGCTextData igcTextDataResponse =
await _igcTextDataCache[reqBody.hashCode]!.data;
await _igcTextDataCache[reqBody.hashCode]!
.data
.timeout((const Duration(seconds: 10)));
// this will happen when the user changes the input while igc is fetching results
if (igcTextDataResponse.originalInput != choreographer.currentText) {
@ -293,6 +295,9 @@ class IgcController {
igcTextData = null;
spanDataController.clearCache();
spanDataController.dispose();
MatrixState.pAnyState.closeAllOverlays(
filter: RegExp(r'span_card_overlay_\d+'),
);
}
dispose() {

View file

@ -57,7 +57,7 @@ class ITController {
choreographer.setState();
}
Duration get animationSpeed => const Duration(milliseconds: 500);
Duration get animationSpeed => const Duration(milliseconds: 300);
Future<void> initializeIT(ITStartData itStartData) async {
_willOpen = true;
@ -136,7 +136,8 @@ class ITController {
// During first IT step, next step will not be set
if (nextITStep == null) {
final ITResponseModel res = await _customInputTranslation(currentText);
final ITResponseModel res = await _customInputTranslation(currentText)
.timeout(const Duration(seconds: 10));
if (sourceText == null) return;
if (res.goldContinuances != null && res.goldContinuances!.isNotEmpty) {

View file

@ -292,12 +292,45 @@ class IGCTextData {
// create a pointer to the current index in the original input
// and iterate until the pointer has reached the end of the input
int currentIndex = 0;
int loops = 0;
final List<PangeaMatch> addedMatches = [];
while (currentIndex < originalInput.characters.length) {
if (loops > 100) {
ErrorHandler.logError(
e: "In constructTokenSpan, infinite loop detected",
data: {
"currentIndex": currentIndex,
"matches": textSpanMatches.map((m) => m.toJson()).toList(),
},
);
throw "In constructTokenSpan, infinite loop detected";
}
// check if the pointer is at a match, and if so, get the index of the match
final int matchIndex = matchRanges.indexWhere(
(range) => currentIndex >= range[0] && currentIndex < range[1],
);
final bool inMatch = matchIndex != -1;
final bool inMatch = matchIndex != -1 &&
!addedMatches.contains(
textSpanMatches[matchIndex],
);
if (matchIndex != -1 &&
addedMatches.contains(
textSpanMatches[matchIndex],
)) {
ErrorHandler.logError(
e: "In constructTokenSpan, currentIndex is in match that has already been added",
data: {
"currentIndex": currentIndex,
"matchIndex": matchIndex,
"matches": textSpanMatches.map((m) => m.toJson()).toList(),
},
);
throw "In constructTokenSpan, currentIndex is in match that has already been added";
}
final prevIndex = currentIndex;
if (inMatch) {
// if the pointer is in a match, then add that match to items
@ -312,13 +345,7 @@ class IGCTextData {
final span = originalInput.characters
.getRange(
match.match.offset,
match.match.offset +
(match.match.choices
?.firstWhere((c) => c.isBestCorrection)
.value
.characters
.length ??
match.match.length),
match.match.offset + match.match.length,
)
.toString();
@ -364,12 +391,8 @@ class IGCTextData {
),
);
currentIndex = match.match.offset +
(match.match.choices
?.firstWhere((c) => c.isBestCorrection)
.value
.length ??
match.match.length);
addedMatches.add(match);
currentIndex = match.match.offset + match.match.length;
} else {
items.add(
getSpanItem(
@ -400,6 +423,20 @@ class IGCTextData {
);
currentIndex = nextIndex;
}
if (prevIndex >= currentIndex) {
ErrorHandler.logError(
e: "In constructTokenSpan, currentIndex is less than prevIndex",
data: {
"currentIndex": currentIndex,
"prevIndex": prevIndex,
"matches": textSpanMatches.map((m) => m.toJson()).toList(),
},
);
throw "In constructTokenSpan, currentIndex is less than prevIndex";
}
loops++;
}
return items;

View file

@ -29,10 +29,6 @@ class ChoicesArray extends StatefulWidget {
final int? selectedChoiceIndex;
final String originalSpan;
/// If null then should not be used
/// We don't want tts in the case of L1 options
final TtsController? tts;
final bool enableAudio;
/// language code for the TTS
@ -62,7 +58,6 @@ class ChoicesArray extends StatefulWidget {
required this.onPressed,
required this.originalSpan,
required this.selectedChoiceIndex,
required this.tts,
this.enableAudio = true,
this.langCode,
this.isActive = true,
@ -111,10 +106,8 @@ class ChoicesArrayState extends State<ChoicesArray> {
? (String value, int index) {
widget.onPressed(value, index);
// TODO - what to pass here as eventID?
if (widget.enableAudio &&
widget.tts != null &&
widget.langCode != null) {
widget.tts?.tryToSpeak(
if (widget.enableAudio && widget.langCode != null) {
TtsController.tryToSpeak(
value,
targetID: null,
langCode: widget.langCode!,

View file

@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/choreographer/models/igc_text_data_model.dart';
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
@ -174,18 +175,29 @@ class PangeaTextController extends TextEditingController {
final choreoSteps = choreographer.choreoRecord.choreoSteps;
List<InlineSpan> inlineSpans = [];
try {
inlineSpans = choreographer.igc.igcTextData!.constructTokenSpan(
choreoSteps: choreoSteps.isNotEmpty &&
choreoSteps.last.acceptedOrIgnoredMatch?.status ==
PangeaMatchStatus.automatic
? choreoSteps
: [],
defaultStyle: style,
onUndo: choreographer.onUndoReplacement,
);
} catch (e) {
choreographer.errorService.setError(
ChoreoError(type: ChoreoErrorType.unknown, raw: e),
);
inlineSpans = [TextSpan(text: text, style: style)];
choreographer.igc.clear();
}
return TextSpan(
style: style,
children: [
...choreographer.igc.igcTextData!.constructTokenSpan(
choreoSteps: choreoSteps.isNotEmpty &&
choreoSteps.last.acceptedOrIgnoredMatch?.status ==
PangeaMatchStatus.automatic
? choreoSteps
: [],
defaultStyle: style,
onUndo: choreographer.onUndoReplacement,
),
...inlineSpans,
TextSpan(text: parts[1], style: style),
],
);

View file

@ -60,12 +60,10 @@ class SpanCardState extends State<SpanCard> {
@override
void dispose() {
tts.stop();
TtsController.stop();
super.dispose();
}
TtsController get tts => widget.scm.choreographer.tts;
//get selected choice
SpanChoice? get selectedChoice {
if (selectedChoiceIndex == null) return null;
@ -263,7 +261,6 @@ class WordMatchContent extends StatelessWidget {
onPressed: (value, index) =>
controller.onChoiceSelect(index),
selectedChoiceIndex: controller.selectedChoiceIndex,
tts: controller.tts,
id: controller.widget.scm.pangeaMatch!.hashCode
.toString(),
langCode: MatrixState.pangeaController.languageController

View file

@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_bar_buttons.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
@ -116,9 +115,13 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
Container(
key: widget.choreographer.itBarLinkAndKey.key,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer,
),
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
padding: const EdgeInsets.all(3),
child: SingleChildScrollView(
child: Column(
children: [
@ -202,12 +205,14 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
if (!itController.isEditingSourceText)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: itController.sourceText != null
? Text(
itController.sourceText!,
textAlign: TextAlign.center,
)
: const LinearProgressIndicator(),
child: !itController.willOpen
? const SizedBox()
: itController.sourceText != null
? Text(
itController.sourceText!,
textAlign: TextAlign.center,
)
: const LinearProgressIndicator(),
),
const SizedBox(height: 8.0),
Container(
@ -387,10 +392,12 @@ class ITChoices extends StatelessWidget {
return const SizedBox();
}
if (controller.currentITStep == null) {
return CircularProgressIndicator(
strokeWidth: 2.0,
color: Theme.of(context).colorScheme.primary,
);
return controller.willOpen
? CircularProgressIndicator(
strokeWidth: 2.0,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox();
}
return ChoicesArray(
id: controller.currentITStep.hashCode.toString(),
@ -414,7 +421,6 @@ class ITChoices extends StatelessWidget {
onPressed: (value, index) => selectContinuance(index, context),
onLongPress: (value, index) => showCard(context, index),
selectedChoiceIndex: null,
tts: controller.choreographer.tts,
langCode: controller.choreographer.pangeaController.languageController
.activeL2Code(),
);
@ -435,23 +441,38 @@ class ITError extends StatelessWidget {
final ErrorCopy errorCopy = ErrorCopy(context, error);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Text(
// Text(
"${errorCopy.title}\n${errorCopy.body}",
// Haga clic en su mensaje para ver los significados de las palabras.
style: TextStyle(
fontStyle: FontStyle.italic,
child: RichText(
text: TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.error_outline,
size: 20,
color: Theme.of(context).colorScheme.error,
),
),
TextSpan(text: " ${errorCopy.title} "),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: IconButton(
onPressed: () {
controller.closeIT();
controller.choreographer.errorService.resetError();
},
icon: const Icon(
Icons.close,
size: 20,
),
),
),
],
style: TextStyle(
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.error,
),
ITRestartButton(controller: controller),
],
),
textAlign: TextAlign.center,
),
);
}

View file

@ -1,27 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../controllers/it_controller.dart';
class ITRestartButton extends StatelessWidget {
ITRestartButton({
super.key,
required this.controller,
});
final ITController controller;
final PangeaController pangeaController = MatrixState.pangeaController;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () async {
controller.choreographer.errorService.resetError();
controller.currentITStep = null;
controller.choreographer.getLanguageHelp();
},
icon: const Icon(Icons.refresh_outlined),
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
import 'package:fluffychat/pangea/choreographer/controllers/contextual_definition_controller.dart';
@ -268,6 +269,13 @@ class PangeaController {
preset: CreateRoomPreset.trustedPrivateChat,
initialState: [
BotOptionsModel(mode: BotMode.directChat).toStateEvent,
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: defaultPowerLevels(
matrixState.client.userID!,
),
),
],
);

View file

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
@ -16,24 +18,32 @@ class FullWidthDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final content = ConstrainedBox(
constraints: FluffyThemes.isColumnMode(context)
? BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
)
: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width,
maxHeight: MediaQuery.of(context).size.height,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: dialogContent,
final isColumnMode = FluffyThemes.isColumnMode(context);
final content = AnimatedSize(
duration: FluffyThemes.animationDuration,
child: ConstrainedBox(
constraints: isColumnMode
? BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
)
: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width,
maxHeight: MediaQuery.of(context).size.height,
),
child: ClipRRect(
borderRadius:
isColumnMode ? BorderRadius.circular(20.0) : BorderRadius.zero,
child: dialogContent,
),
),
);
return FluffyThemes.isColumnMode(context)
? Dialog(child: content)
: Dialog.fullscreen(child: content);
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5),
child: isColumnMode
? Dialog(child: content)
: Dialog.fullscreen(child: content),
);
}
}

View file

@ -12,7 +12,6 @@ import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
@ -23,7 +22,7 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
class MessageDataController extends BaseController {
late PangeaController _pangeaController;
final Map<int, Future<List<PangeaToken>>> _tokensCache = {};
final Map<int, Future<TokensResponseModel>> _tokensCache = {};
final Map<int, Future<PangeaRepresentation>> _representationCache = {};
late Timer _cacheTimer;
@ -54,7 +53,7 @@ class MessageDataController extends BaseController {
/// get tokens from the server
/// if repEventId is not null, send the tokens to the room
Future<List<PangeaToken>> _getTokens({
Future<TokensResponseModel> _getTokens({
required String? repEventId,
required TokensRequestModel req,
required Room? room,
@ -83,13 +82,13 @@ class MessageDataController extends BaseController {
);
}
return res.tokens;
return res;
}
/// get tokens from the server
/// first check if the tokens are in the cache
/// if repEventId is not null, send the tokens to the room
Future<List<PangeaToken>> getTokens({
Future<TokensResponseModel> getTokens({
required String? repEventId,
required TokensRequestModel req,
required Room? room,

View file

@ -93,7 +93,7 @@ class RepresentationEvent {
if (tokenEvents.isEmpty) return null;
if (tokenEvents.length > 1) {
debugger(when: kDebugMode);
// debugger(when: kDebugMode);
Sentry.addBreadcrumb(
Breadcrumb(
message:
@ -162,7 +162,7 @@ class RepresentationEvent {
),
);
}
final List<PangeaToken> res =
final TokensResponseModel res =
await MatrixState.pangeaController.messageData.getTokens(
repEventId: _event?.eventId,
room: _event?.room ?? parentMessageEvent.room,
@ -180,7 +180,7 @@ class RepresentationEvent {
),
);
return res;
return res.tokens;
}
Future<void> sendTokensEvent(

View file

@ -1,6 +1,7 @@
import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
class TokensRequestModel {
/// the text to be tokenized
@ -24,16 +25,16 @@ class TokensRequestModel {
TokensRequestModel({
required this.fullText,
required this.langCode,
required this.senderL1,
required this.senderL2,
this.langCode,
});
Map<String, dynamic> toJson() => {
ModelKey.fullText: fullText,
ModelKey.userL1: senderL1,
ModelKey.userL2: senderL2,
ModelKey.langCode: langCode,
ModelKey.langCode: langCode ?? LanguageKeys.unknownLanguage,
};
// override equals and hashcode

View file

@ -49,8 +49,12 @@ class MessageTextUtil {
return null;
}
if (_tokenPositionsCache.containsKey(pangeaMessageEvent.eventId)) {
return _tokenPositionsCache[pangeaMessageEvent.eventId]!
final cacheKey = pangeaMessageEvent.event
.getDisplayEvent(pangeaMessageEvent.timeline)
.eventId;
if (_tokenPositionsCache.containsKey(cacheKey)) {
return _tokenPositionsCache[cacheKey]!
.map(
(t) => TokenPosition(
start: t.start,
@ -154,7 +158,7 @@ class MessageTextUtil {
continue;
}
_tokenPositionsCache[pangeaMessageEvent.eventId] = tokenPositions;
_tokenPositionsCache[cacheKey] = tokenPositions;
return tokenPositions;
} catch (err, s) {

View file

@ -274,10 +274,20 @@ extension EventsRoomExtension on Room {
}) async {
Uint8List? bytes = avatar;
if (avatarURL != null && bytes == null) {
final resp = await http
.get(Uri.parse(avatarURL))
.timeout(const Duration(seconds: 5));
bytes = resp.bodyBytes;
try {
final resp = await http
.get(Uri.parse(avatarURL))
.timeout(const Duration(seconds: 5));
bytes = resp.bodyBytes;
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"avatarURL": avatarURL,
},
);
}
}
MatrixFile? file;

View file

@ -1,96 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
class BottomNavLayout extends StatelessWidget {
final Widget mainView;
const BottomNavLayout({
super.key,
required this.mainView,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: mainView,
bottomNavigationBar: const BottomNavBar(),
);
}
}
class BottomNavBar extends StatefulWidget {
const BottomNavBar({
super.key,
});
@override
BottomNavBarState createState() => BottomNavBarState();
}
class BottomNavBarState extends State<BottomNavBar> {
int get selectedIndex {
final route = GoRouterState.of(context).fullPath.toString();
if (route.contains("settings")) {
return 2;
}
if (route.contains('homepage')) {
return 0;
}
return 1;
}
void onItemTapped(int index) {
switch (index) {
case 0:
context.go('/homepage');
break;
case 1:
context.go('/rooms');
break;
case 2:
context.go('/rooms/settings');
break;
}
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.primary.withAlpha(50),
),
),
),
child: BottomNavigationBar(
iconSize: 16.0,
onTap: onItemTapped,
selectedItemColor: Theme.of(context).colorScheme.primary,
selectedFontSize: 14.0,
unselectedFontSize: 14.0,
currentIndex: selectedIndex,
items: [
BottomNavigationBarItem(
icon: const Icon(Icons.home_outlined),
activeIcon: const Icon(Icons.home),
label: L10n.of(context).home,
),
BottomNavigationBarItem(
icon: const Icon(Icons.chat_bubble_outline),
activeIcon: const Icon(Icons.chat_bubble),
label: L10n.of(context).chats,
),
BottomNavigationBarItem(
icon: const Icon(Icons.settings_outlined),
activeIcon: const Icon(Icons.settings),
label: L10n.of(context).settings,
),
],
),
);
}
}

View file

@ -35,7 +35,6 @@ class SettingsLearning extends StatefulWidget {
class SettingsLearningController extends State<SettingsLearning> {
PangeaController pangeaController = MatrixState.pangeaController;
late Profile _profile;
final tts = TtsController();
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
String? languageMatchError;
@ -46,12 +45,12 @@ class SettingsLearningController extends State<SettingsLearning> {
void initState() {
super.initState();
_profile = pangeaController.userController.profile.copy();
tts.setAvailableLanguages().then((_) => setState(() {}));
TtsController.setAvailableLanguages().then((_) => setState(() {}));
}
@override
void dispose() {
tts.dispose();
TtsController.stop();
scrollController.dispose();
super.dispose();
}

View file

@ -259,8 +259,9 @@ class UserSettingsState extends State<UserSettingsPage> {
MatrixState.pangeaController.pLanguageStore.baseOptions;
bool get _hasIdenticalLanguages =>
_systemLanguage != null &&
_systemLanguage?.langCodeShort == selectedTargetLanguage?.langCodeShort;
selectedBaseLanguage != null &&
selectedTargetLanguage?.langCodeShort ==
selectedBaseLanguage?.langCodeShort;
@override
Widget build(BuildContext context) => UserSettingsView(controller: this);

View file

@ -15,25 +15,6 @@ enum ActivityTypeEnum {
}
extension ActivityTypeExtension on ActivityTypeEnum {
String get string {
switch (this) {
case ActivityTypeEnum.wordMeaning:
return 'word_meaning';
case ActivityTypeEnum.wordFocusListening:
return 'word_focus_listening';
case ActivityTypeEnum.hiddenWordListening:
return 'hidden_word_listening';
case ActivityTypeEnum.lemmaId:
return 'lemma_id';
case ActivityTypeEnum.emoji:
return 'emoji';
case ActivityTypeEnum.morphId:
return 'morph_id';
case ActivityTypeEnum.messageMeaning:
return 'message_meaning'; // TODO: Add to L10n
}
}
bool get hiddenType {
switch (this) {
case ActivityTypeEnum.wordMeaning:

View file

@ -83,7 +83,7 @@ class MessageActivityRequest {
'message_tokens': messageTokens.map((e) => e.toJson()).toList(),
'activity_quality_feedback': activityQualityFeedback?.toJson(),
'target_tokens': targetTokens.map((e) => e.toJson()).toList(),
'target_type': targetType.string,
'target_type': targetType.name,
'target_morph_feature': targetMorphFeature,
};
}

View file

@ -326,7 +326,7 @@ class PracticeActivityModel {
Map<String, dynamic> toJson() {
return {
'lang_code': langCode,
'activity_type': activityType.string,
'activity_type': activityType.name,
'content': multipleChoiceContent?.toJson(),
'target_tokens': targetTokens.map((e) => e.toJson()).toList(),
'match_content': matchContent?.toJson(),

View file

@ -26,7 +26,7 @@ class PracticeRecordRepo {
}
static void clean() {
final Iterable<String> keys = _storage.getKeys();
final keys = _storage.getKeys();
if (keys.length > 300) {
final entries = keys
.map((key) {

View file

@ -2,6 +2,8 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
@ -60,13 +62,22 @@ class PracticeTarget {
userL2.hashCode;
static PracticeTarget fromJson(Map<String, dynamic> json) {
final type = ActivityTypeEnum.values.firstWhereOrNull(
(v) => json['activityType'] == v.name,
);
if (type == null) {
throw Exception(
"ActivityTypeEnum ${json['activityType']} not found in enum",
);
}
return PracticeTarget(
tokens:
(json['tokens'] as List).map((e) => PangeaToken.fromJson(e)).toList(),
activityType: ActivityTypeEnum.values[json['activityType']],
activityType: type,
morphFeature: json['morphFeature'] == null
? null
: MorphFeaturesEnum.values[json['morphFeature']],
: MorphFeaturesEnumExtension.fromString(json['morphFeature']),
userL2: json['userL2'],
);
}
@ -83,7 +94,7 @@ class PracticeTarget {
//unique condensed deterministic key for local storage
String get storageKey {
return tokens.map((e) => e.text.content).join() +
activityType.string +
activityType.name +
(morphFeature?.name ?? "");
}

View file

@ -0,0 +1,395 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PublicRoomBottomSheet extends StatefulWidget {
final String? roomAlias;
final BuildContext outerContext;
final PublicRoomsChunk? chunk;
final List<String>? via;
PublicRoomBottomSheet({
this.roomAlias,
required this.outerContext,
this.chunk,
this.via,
super.key,
}) {
assert(roomAlias != null || chunk != null);
}
static Future<bool?> show({
required BuildContext context,
String? roomAlias,
PublicRoomsChunk? chunk,
List<String>? via,
}) async {
final room = MatrixState.pangeaController.matrixState.client
.getRoomById(chunk!.roomId);
if (room != null && room.membership == Membership.join) {
context.go("/rooms?spaceId=${room.id}");
return null;
}
return showAdaptiveBottomSheet(
context: context,
builder: (context) => PublicRoomBottomSheet(
roomAlias: roomAlias,
chunk: chunk,
via: via,
outerContext: context,
),
);
}
@override
State<StatefulWidget> createState() => PublicRoomBottomSheetState();
}
class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
BuildContext get outerContext => widget.outerContext;
PublicRoomsChunk? get chunk => widget.chunk;
String? get roomAlias => widget.roomAlias;
List<String>? get via => widget.via;
final TextEditingController _codeController = TextEditingController();
late Client client;
@override
void initState() {
super.initState();
client = Matrix.of(outerContext).client;
}
@override
void dispose() {
_codeController.dispose();
super.dispose();
}
Room? get room => client.getRoomById(chunk!.roomId);
bool get _isRoomMember => room != null && room!.membership == Membership.join;
bool get _isKnockRoom => widget.chunk?.joinRule == 'knock';
Future<void> _joinWithCode() async {
final resp =
await MatrixState.pangeaController.classController.joinClasswithCode(
context,
_codeController.text,
notFoundError: L10n.of(context).notTheCodeError,
);
if (!resp.isError) {
Navigator.of(context).pop(true);
}
}
void _goToRoom(String roomID) {
if (chunk?.roomType != 'm.space' && !client.getRoomById(roomID)!.isSpace) {
outerContext.go("/rooms/$roomID");
} else {
context.go('/rooms?spaceId=$roomID');
}
}
Future<void> _joinRoom() async {
if (_isRoomMember) {
_goToRoom(room!.id);
Navigator.of(context).pop();
return;
}
final result = await showFutureLoadingDialog<String>(
context: context,
future: () async {
final roomId = await client.joinRoom(
roomAlias ?? chunk!.roomId,
serverName: via,
);
final room = client.getRoomById(roomId);
if (room == null || room.membership != Membership.join) {
await client.waitForRoomInSync(roomId, join: true);
}
return roomId;
},
);
if (result.result != null) {
_goToRoom(result.result!);
Navigator.of(context).pop(true);
}
}
Future<void> _knockRoom() async {
if (_isRoomMember) {
_goToRoom(room!.id);
Navigator.of(context).pop();
return;
}
await showFutureLoadingDialog<String>(
context: context,
future: () async => client.knockRoom(
roomAlias ?? chunk!.roomId,
serverName: via,
),
onSuccess: () => L10n.of(context).knockSpaceSuccess,
delay: false,
);
}
bool testRoom(PublicRoomsChunk r) => r.canonicalAlias == roomAlias;
Future<PublicRoomsChunk> search() async {
final chunk = this.chunk;
if (chunk != null) return chunk;
final query = await Matrix.of(outerContext).client.queryPublicRooms(
server: roomAlias!.domain,
filter: PublicRoomQueryFilter(
genericSearchTerm: roomAlias,
),
);
if (!query.chunk.any(testRoom)) {
throw (L10n.of(outerContext).noRoomsFound);
}
return query.chunk.firstWhere(testRoom);
}
@override
Widget build(BuildContext context) {
final roomAlias = this.roomAlias ?? chunk?.canonicalAlias;
return SafeArea(
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(
chunk?.name ?? roomAlias ?? chunk?.roomId ?? 'Unknown',
overflow: TextOverflow.fade,
),
actions: [
Center(
child: CloseButton(
onPressed: Navigator.of(context, rootNavigator: false).pop,
),
),
],
),
body: FutureBuilder<PublicRoomsChunk>(
future: search(),
builder: (context, snapshot) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 32.0,
children: [
Row(
spacing: 16.0,
children: [
Avatar(
mxContent: chunk?.avatarUrl,
name: chunk?.name,
size: 160.0,
borderRadius: BorderRadius.circular(24.0),
),
Expanded(
child: SizedBox(
height: 160.0,
child: Column(
spacing: 16.0,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 8.0,
children: [
const Icon(Icons.group),
Text(
L10n.of(context).countParticipants(
chunk?.numJoinedMembers ?? 1,
),
),
],
),
if (chunk?.topic != null)
Flexible(
child: SingleChildScrollView(
child: Text(
chunk!.topic!,
softWrap: true,
textAlign: TextAlign.start,
maxLines: null,
),
),
),
],
),
),
),
],
),
Column(
spacing: 8.0,
children: _isKnockRoom
? [
Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(24),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _codeController,
decoration: InputDecoration(
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText:
L10n.of(context).enterSpaceCode,
contentPadding:
const EdgeInsets.symmetric(
horizontal: 16.0,
),
hintStyle: TextStyle(
color: Theme.of(context).hintColor,
),
),
),
),
Container(
decoration: BoxDecoration(
border: Border(
left: BorderSide(
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
child: ElevatedButton(
onPressed: _joinWithCode,
style: ElevatedButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.zero,
bottomLeft: Radius.zero,
topRight: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
),
child: Text(L10n.of(context).join),
),
),
],
),
),
ElevatedButton(
onPressed: _knockRoom,
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Symbols.door_open,
size: 20.0,
),
Text(L10n.of(context).askToJoin),
],
),
),
if (roomAlias != null)
ElevatedButton(
onPressed: () {
FluffyShare.share(
"${Environment.frontendURL}/#/join_with_alias?alias=${Uri.encodeComponent(roomAlias)}",
context,
);
Navigator.of(context).pop();
},
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.share_outlined,
size: 20.0,
),
Flexible(
child: Text(
L10n.of(context).shareSpaceLink,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
]
: [
ElevatedButton(
onPressed: _joinRoom,
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.join_full_outlined,
size: 20.0,
),
Text(L10n.of(context).join),
],
),
),
if (roomAlias != null)
ElevatedButton(
onPressed: () {
FluffyShare.share(
"${Environment.frontendURL}/#/join_with_alias?alias=${Uri.encodeComponent(roomAlias)}",
context,
);
Navigator.of(context).pop();
},
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.share_outlined,
size: 20.0,
),
Flexible(
child: Text(
L10n.of(context).shareSpaceLink,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
],
),
);
},
),
),
);
}
}

View file

@ -4,7 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class PublicSpaceCard extends StatelessWidget {
@ -24,10 +24,10 @@ class PublicSpaceCard extends StatelessWidget {
final theme = Theme.of(context);
return PressableButton(
onPressed: () => PublicRoomDialog.show(
context: context,
onPressed: () => PublicRoomBottomSheet.show(
roomAlias: space.canonicalAlias ?? space.roomId,
chunk: space,
context: context,
),
borderRadius: BorderRadius.circular(24.0),
color: theme.brightness == Brightness.dark
@ -74,7 +74,7 @@ class PublicSpaceCard extends StatelessWidget {
padding: const EdgeInsets.all(8.0),
child: Column(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(

View file

@ -176,7 +176,7 @@ class PublicSpacesAreaState extends State<PublicSpacesArea> {
key: const ValueKey('title'),
children: [
Text(
L10n.of(context).publicSpacesTitle,
L10n.of(context).findYourPeople,
style: isColumnMode
? theme.textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.bold)

View file

@ -11,6 +11,8 @@ import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
extension SpacesClientExtension on Client {
Future<String> createPangeaSpace({
required String name,
required String introChatName,
required String announcementsChatName,
Visibility visibility = Visibility.private,
JoinRules joinRules = JoinRules.public,
Uint8List? avatar,
@ -24,6 +26,7 @@ extension SpacesClientExtension on Client {
powerLevelContentOverride: {'events_default': 100},
initialState: [
..._spaceInitialState(
userID!,
joinCode,
joinRules: joinRules,
),
@ -38,7 +41,11 @@ extension SpacesClientExtension on Client {
final space = await _waitForRoom(roomId);
if (space == null) return roomId;
await _addDefaultSpaceChats(space: space);
await _addDefaultSpaceChats(
space: space,
introductionsName: introChatName,
announcementsName: announcementsChatName,
);
return roomId;
}
@ -108,6 +115,13 @@ extension SpacesClientExtension on Client {
throw Exception('Failed to create default space chats');
}
for (final roomId in roomIds) {
final room = getRoomById(roomId);
if (room == null) {
await waitForRoomInSync(roomId, join: true);
}
}
final addIntroChatFuture = space.pangeaSetSpaceChild(
roomIds[0],
);
@ -123,6 +137,7 @@ extension SpacesClientExtension on Client {
}
List<StateEvent> _spaceInitialState(
String userID,
String joinCode, {
required JoinRules joinRules,
}) {
@ -130,15 +145,7 @@ extension SpacesClientExtension on Client {
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: {
'events': {
EventTypes.SpaceChild: 50,
},
'users_default': 0,
'users': {
userID: SpaceConstants.powerLevelOfAdmin,
},
},
content: defaultSpacePowerLevels(userID),
),
StateEvent(
type: EventTypes.RoomJoinRules,

View file

@ -24,55 +24,37 @@ import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
class TtsController {
final ChatController? chatController;
TtsController({this.chatController}) {
static void initialize() {
setAvailableLanguages();
_languageSubscription =
MatrixState.pangeaController.userController.stateStream.listen(
(_) => setAvailableLanguages(),
);
}
List<String> _availableLangCodes = [];
StreamSubscription? _languageSubscription;
static List<String> _availableLangCodes = [];
final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts();
final TextToSpeech _alternativeTTS = TextToSpeech();
final StreamController<bool> loadingChoreoStream =
static final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts();
static final TextToSpeech _alternativeTTS = TextToSpeech();
static final StreamController<bool> loadingChoreoStream =
StreamController<bool>.broadcast();
bool get _useAlternativeTTS {
static bool get _useAlternativeTTS {
return PlatformInfos.isWindows;
}
Future<void> dispose() async {
await _tts.stop();
await _languageSubscription?.cancel();
await loadingChoreoStream.close();
}
void _onError(dynamic message) {
// the package treats this as an error, but it's not
// don't send to sentry
if (message == 'canceled' || message == 'interrupted') {
return;
static Future<void> _onError(dynamic message) async {
if (message != 'canceled' && message != 'interrupted') {
ErrorHandler.logError(
e: 'TTS error',
data: {
'message': message,
},
);
}
ErrorHandler.logError(
e: 'TTS error',
data: {
'message': message,
},
);
}
Future<void> setAvailableLanguages() async {
static Future<void> setAvailableLanguages() async {
try {
if (_useAlternativeTTS) {
await _setAvailableAltLanguages();
} else {
_tts.setErrorHandler(_onError);
await _tts.awaitSpeakCompletion(true);
await _setAvailableBaseLanguages();
}
@ -86,7 +68,7 @@ class TtsController {
}
}
Future<void> _setAvailableBaseLanguages() async {
static Future<void> _setAvailableBaseLanguages() async {
final voices = (await _tts.getVoices) as List?;
_availableLangCodes = (voices ?? [])
.map((v) {
@ -100,12 +82,12 @@ class TtsController {
.toList();
}
Future<void> _setAvailableAltLanguages() async {
static Future<void> _setAvailableAltLanguages() async {
final languages = await _alternativeTTS.getLanguages();
_availableLangCodes = languages.toSet().toList();
}
Future<void> _setSpeakingLanguage(String langCode) async {
static Future<void> _setSpeakingLanguage(String langCode) async {
String? selectedLangCode;
final langCodeShort = langCode.split("-").first;
if (_availableLangCodes.contains(langCode)) {
@ -132,7 +114,7 @@ class TtsController {
}
}
Future<void> stop() async {
static Future<void> stop() async {
try {
// return type is dynamic but apparent its supposed to be 1
// https://pub.dev/packages/flutter_tts
@ -157,26 +139,67 @@ class TtsController {
}
}
/// A safer version of speak, that handles the case of
/// the language not being supported by the TTS engine
Future<void> tryToSpeak(
static VoidCallback? _onStop;
static Future<void> tryToSpeak(
String text, {
required String langCode,
// Target ID for where to show warning popup
String? targetID,
BuildContext? context,
ChatController? chatController,
VoidCallback? onStart,
VoidCallback? onStop,
}) async {
final prevOnStop = _onStop;
_onStop = onStop;
_tts.setErrorHandler((message) {
_onError(message);
prevOnStop?.call();
});
onStart?.call();
await _tryToSpeak(
text,
langCode: langCode,
targetID: targetID,
context: context,
chatController: chatController,
onStart: onStart,
onStop: onStop,
);
onStop?.call();
}
/// A safer version of speak, that handles the case of
/// the language not being supported by the TTS engine
static Future<void> _tryToSpeak(
String text, {
required String langCode,
// Target ID for where to show warning popup
String? targetID,
BuildContext? context,
ChatController? chatController,
VoidCallback? onStart,
VoidCallback? onStop,
}) async {
chatController?.stopMediaStream.add(null);
await _setSpeakingLanguage(langCode);
final enableTTS = MatrixState
.pangeaController.userController.profile.toolSettings.enableTTS;
if (enableTTS) {
final token = PangeaTokenText(
offset: 0,
content: text,
length: text.length,
);
onStart?.call();
await (_isLangFullySupported(langCode)
? _speak(
text,
@ -191,31 +214,33 @@ class TtsController {
} else if (targetID != null && context != null) {
await _showTTSDisabledPopup(context, targetID);
}
onStop?.call();
}
Future<void> _speak(
static Future<void> _speak(
String text,
String langCode,
List<PangeaTokenText> tokens,
) async {
try {
stop();
await stop();
text = text.toLowerCase();
Logs().i('Speaking: $text, langCode: $langCode');
final result = await Future(
() => (_useAlternativeTTS
? _alternativeTTS.speak(text)
: _tts.speak(text))
.timeout(
const Duration(seconds: 5),
onTimeout: () {
ErrorHandler.logError(
e: "Timeout on tts.speak",
data: {"text": text},
);
},
),
? _alternativeTTS.speak(text)
: _tts.speak(text)),
// .timeout(
// const Duration(seconds: 5),
// // onTimeout: () {
// // ErrorHandler.logError(
// // e: "Timeout on tts.speak",
// // data: {"text": text},
// // );
// // },
// ),
);
Logs().i('Finished speaking: $text, result: $result');
@ -241,10 +266,12 @@ class TtsController {
},
);
await _speakFromChoreo(text, langCode, tokens);
} finally {
stop();
}
}
Future<void> _speakFromChoreo(
static Future<void> _speakFromChoreo(
String text,
String langCode,
List<PangeaTokenText> tokens,
@ -252,7 +279,7 @@ class TtsController {
TextToSpeechResponse? ttsRes;
try {
loadingChoreoStream.add(true);
ttsRes = await chatController?.pangeaController.textToSpeech.get(
ttsRes = await MatrixState.pangeaController.textToSpeech.get(
TextToSpeechRequest(
text: text,
langCode: langCode,
@ -304,7 +331,7 @@ class TtsController {
}
}
bool _isLangFullySupported(String langCode) {
static bool _isLangFullySupported(String langCode) {
if (_availableLangCodes.contains(langCode)) {
return true;
}
@ -317,7 +344,7 @@ class TtsController {
return _availableLangCodes.any((lang) => lang.startsWith(langCodeShort));
}
Future<void> _showTTSDisabledPopup(
static Future<void> _showTTSDisabledPopup(
BuildContext context,
String targetID,
) async =>

View file

@ -39,9 +39,6 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
bool _isHovered = false;
bool _isPlaying = false;
TtsController get tts =>
widget.overlayController.widget.chatController.choreographer.tts;
bool get isSelected => widget.isSelected;
bool? get isCorrect => widget.isCorrect;
@ -52,7 +49,7 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
}
if (_isPlaying) {
await tts.stop();
await TtsController.stop();
if (mounted) {
setState(() => _isPlaying = false);
}
@ -64,7 +61,7 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
final l2 =
MatrixState.pangeaController.languageController.activeL2Code();
if (l2 != null) {
await tts.tryToSpeak(
await TtsController.tryToSpeak(
widget.audioContent!,
context: context,
targetID: 'word-audio-button',

View file

@ -27,6 +27,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart';
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
@ -546,7 +547,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
) ==
false ||
!hideWordCardContent) {
widget.chatController.choreographer.tts.tryToSpeak(
TtsController.tryToSpeak(
token.text.content,
targetID: null,
langCode: pangeaMessageEvent!.messageDisplayLangCode,

View file

@ -9,7 +9,7 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/utils/report_message.dart';
class OverlayHeader extends StatelessWidget {
class OverlayHeader extends StatefulWidget {
final ChatController controller;
const OverlayHeader({
@ -17,6 +17,21 @@ class OverlayHeader extends StatelessWidget {
super.key,
});
@override
State<OverlayHeader> createState() => OverlayHeaderState();
}
class OverlayHeaderState extends State<OverlayHeader> {
ChatController get controller => widget.controller;
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
@ -35,84 +50,106 @@ class OverlayHeader extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Symbols.reply_all),
tooltip: l10n.reply,
onPressed: controller.replyAction,
color: theme.colorScheme.primary,
),
IconButton(
icon: const Icon(Symbols.forward),
tooltip: l10n.forward,
onPressed: controller.forwardEventsAction,
color: theme.colorScheme.primary,
),
if (controller.selectedEvents.length == 1 &&
controller.selectedEvents.single.messageType == MessageTypes.Text)
IconButton(
icon: const Icon(Icons.copy_outlined),
tooltip: l10n.copy,
onPressed: controller.copyEventsAction,
color: theme.colorScheme.primary,
),
if (controller.canSaveSelectedEvent)
// Use builder context to correctly position the share dialog on iPad
Builder(
builder: (context) => IconButton(
icon: const Icon(Symbols.download),
tooltip: L10n.of(context).download,
onPressed: () => controller.saveSelectedEvent(context),
color: theme.colorScheme.primary,
Expanded(
child: Scrollbar(
thumbVisibility: true,
controller: _scrollController,
child: Align(
alignment: Alignment.centerRight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _scrollController,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Symbols.reply_all),
tooltip: l10n.reply,
onPressed: controller.replyAction,
color: theme.colorScheme.primary,
),
IconButton(
icon: const Icon(Symbols.forward),
tooltip: l10n.forward,
onPressed: controller.forwardEventsAction,
color: theme.colorScheme.primary,
),
if (controller.selectedEvents.length == 1 &&
controller.selectedEvents.single.messageType ==
MessageTypes.Text)
IconButton(
icon: const Icon(Icons.copy_outlined),
tooltip: l10n.copy,
onPressed: controller.copyEventsAction,
color: theme.colorScheme.primary,
),
if (controller.canSaveSelectedEvent)
// Use builder context to correctly position the share dialog on iPad
Builder(
builder: (context) => IconButton(
icon: const Icon(Symbols.download),
tooltip: L10n.of(context).download,
onPressed: () =>
controller.saveSelectedEvent(context),
color: theme.colorScheme.primary,
),
),
if (controller.canPinSelectedEvents)
IconButton(
icon: const Icon(Icons.push_pin_outlined),
onPressed: controller.pinEvent,
tooltip: l10n.pinMessage,
color: theme.colorScheme.primary,
),
if (controller.canEditSelectedEvents &&
!controller.selectedEvents.first.isActivityMessage)
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: l10n.edit,
onPressed: controller.editSelectedEventAction,
color: theme.colorScheme.primary,
),
if (controller.canRedactSelectedEvents)
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: l10n.redactMessage,
onPressed: controller.redactEventsAction,
color: theme.colorScheme.primary,
),
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Icons.shield_outlined),
tooltip: l10n.reportMessage,
onPressed: () {
final event = controller.selectedEvents.first;
controller.clearSelectedEvents();
reportEvent(
event,
controller,
controller.context,
);
},
color: theme.colorScheme.primary,
),
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Icons.info_outlined),
tooltip: l10n.messageInfo,
color: theme.colorScheme.primary,
onPressed: () {
controller.showEventInfo();
controller.clearSelectedEvents();
},
),
],
),
),
),
),
),
if (controller.canPinSelectedEvents)
IconButton(
icon: const Icon(Icons.push_pin_outlined),
onPressed: controller.pinEvent,
tooltip: l10n.pinMessage,
color: theme.colorScheme.primary,
),
if (controller.canEditSelectedEvents &&
!controller.selectedEvents.first.isActivityMessage)
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: l10n.edit,
onPressed: controller.editSelectedEventAction,
color: theme.colorScheme.primary,
),
if (controller.canRedactSelectedEvents)
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: l10n.redactMessage,
onPressed: controller.redactEventsAction,
color: theme.colorScheme.primary,
),
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Icons.shield_outlined),
tooltip: l10n.reportMessage,
onPressed: () {
final event = controller.selectedEvents.first;
controller.clearSelectedEvents();
reportEvent(
event,
controller,
controller.context,
);
},
color: theme.colorScheme.primary,
),
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Icons.info_outlined),
tooltip: l10n.messageInfo,
color: theme.colorScheme.primary,
onPressed: () {
controller.showEventInfo();
controller.clearSelectedEvents();
},
),
),
],
),
);

View file

@ -80,9 +80,11 @@ class OverlayMessage extends StatelessWidget {
previousEvent!.senderId == event.senderId &&
previousEvent!.originServerTs.sameEnvironment(event.originServerTs);
final textColor = ownMessage
? ThemeData.dark().colorScheme.onPrimary
: theme.colorScheme.onSurface;
final textColor = event.isActivityMessage
? ThemeData.light().colorScheme.onPrimary
: ownMessage
? ThemeData.dark().colorScheme.onPrimary
: theme.colorScheme.onSurface;
final linkColor = theme.brightness == Brightness.light
? theme.colorScheme.primary

View file

@ -13,7 +13,6 @@ import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_record.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
@ -80,9 +79,6 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
}
}
TtsController get tts =>
widget.overlayController.widget.chatController.choreographer.tts;
void updateChoice(String value, int index) {
final bool isCorrect =
widget.currentActivity.multipleChoiceContent!.isCorrect(value, index);
@ -232,7 +228,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
text: practiceActivity.multipleChoiceContent!.answers.first,
uniqueID: "audio-activity-${widget.event.eventId}",
langCode: widget
.overlayController.pangeaMessageEvent?.messageDisplayLangCode,
.overlayController.pangeaMessageEvent!.messageDisplayLangCode,
),
if (practiceActivity.activityType ==
ActivityTypeEnum.hiddenWordListening)
@ -251,8 +247,8 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
choices: choices(context),
isActive: true,
id: currentRecordModel?.hashCode.toString(),
tts: practiceActivity.activityType.includeTTSOnClick ? tts : null,
enableAudio: !widget.overlayController.isPlayingAudio,
enableAudio: !widget.overlayController.isPlayingAudio &&
practiceActivity.activityType.includeTTSOnClick,
langCode:
MatrixState.pangeaController.languageController.activeL2Code(),
getDisplayCopy: _getDisplayCopy,

View file

@ -15,6 +15,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_activity_model.da
import 'package:fluffychat/pangea/practice_activities/practice_generation_repo.dart';
import 'package:fluffychat/pangea/practice_activities/practice_record.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart';
@ -231,7 +232,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
widget.overlayController
.onActivityFinish(currentActivity!.activityType, null);
widget.overlayController.widget.chatController.choreographer.tts.stop();
TtsController.stop();
} catch (e, s) {
_onError();
debugger(when: kDebugMode);

View file

@ -1,8 +1,9 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -11,7 +12,7 @@ class WordAudioButton extends StatefulWidget {
final bool isSelected;
final double baseOpacity;
final String uniqueID;
final String? langCode;
final String langCode;
final EdgeInsets? padding;
/// If defined, this callback will be called instead of the default one
@ -21,10 +22,10 @@ class WordAudioButton extends StatefulWidget {
super.key,
required this.text,
required this.uniqueID,
required this.langCode,
this.isSelected = false,
this.baseOpacity = 1,
this.callbackOverride,
this.langCode,
this.padding,
});
@ -33,8 +34,19 @@ class WordAudioButton extends StatefulWidget {
}
class WordAudioButtonState extends State<WordAudioButton> {
final TtsController tts = TtsController();
late TtsController tts;
bool _isPlaying = false;
bool _isLoading = false;
StreamSubscription? _loadingChoreoSubscription;
@override
void initState() {
super.initState();
_loadingChoreoSubscription =
TtsController.loadingChoreoStream.stream.listen((val) {
if (mounted) setState(() => _isLoading = val);
});
}
@override
void didUpdateWidget(covariant WordAudioButton oldWidget) {
@ -47,7 +59,8 @@ class WordAudioButtonState extends State<WordAudioButton> {
@override
void dispose() {
tts.dispose();
TtsController.stop();
_loadingChoreoSubscription?.cancel();
super.dispose();
}
@ -71,45 +84,34 @@ class WordAudioButtonState extends State<WordAudioButton> {
onTap: widget.callbackOverride ??
() async {
if (_isPlaying) {
await tts.stop();
if (mounted) {
setState(() => _isPlaying = false);
}
await TtsController.stop();
} else {
if (mounted) {
setState(() => _isPlaying = true);
}
try {
if (widget.langCode != null) {
await tts.tryToSpeak(
widget.text,
context: context,
targetID: 'word-audio-button-${widget.uniqueID}',
langCode: widget.langCode!,
);
}
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"text": widget.text,
},
);
} finally {
if (mounted) {
setState(() => _isPlaying = false);
}
}
await TtsController.tryToSpeak(
widget.text,
context: context,
targetID: 'word-audio-button-${widget.uniqueID}',
langCode: widget.langCode,
onStart: () => setState(() => _isPlaying = true),
onStop: () => setState(() => _isPlaying = false),
);
}
},
child: Padding(
padding: widget.padding ?? const EdgeInsets.all(0.0),
child: Icon(
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
color:
_isPlaying ? Theme.of(context).colorScheme.primary : null,
),
child: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 3,
),
)
: Icon(
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
color: _isPlaying
? Theme.of(context).colorScheme.primary
: null,
),
),
),
),

View file

@ -1,139 +1,46 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
class WordTextWithAudioButton extends StatefulWidget {
class WordTextWithAudioButton extends StatelessWidget {
final String text;
final String uniqueID;
final TextStyle? style;
final double? iconSize;
final String langCode;
const WordTextWithAudioButton({
super.key,
required this.text,
required this.uniqueID,
required this.langCode,
this.style,
this.iconSize,
});
@override
WordAudioButtonState createState() => WordAudioButtonState();
}
class WordAudioButtonState extends State<WordTextWithAudioButton> {
// initialize as null because we don't know if we need to load
// audio from choreo yet. This shall remain null if user device support
// text to speech
final bool? _isLoadingAudio = null;
final TtsController tts = TtsController();
bool _isPlaying = false;
bool _isLoading = false;
StreamSubscription? _loadingChoreoSubscription;
@override
void initState() {
super.initState();
_loadingChoreoSubscription = tts.loadingChoreoStream.stream.listen((val) {
if (mounted) setState(() => _isLoading = val);
});
}
@override
void dispose() {
_loadingChoreoSubscription?.cancel();
tts.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey('text-audio-button-${widget.uniqueID}')
.link,
child: MouseRegion(
key: MatrixState.pAnyState
.layerLinkAndKey('text-audio-button-${widget.uniqueID}')
.key,
cursor: SystemMouseCursors.click,
onEnter: (event) => setState(() {}),
onExit: (event) => setState(() {}),
child: GestureDetector(
onTap: () async {
if (_isLoadingAudio == true) {
return;
}
if (_isPlaying) {
await tts.stop();
if (mounted) {
setState(() => _isPlaying = false);
}
} else {
if (mounted) {
setState(() => _isPlaying = true);
}
try {
final l2 = MatrixState.pangeaController.languageController
.activeL2Code();
if (l2 != null) {
await tts.tryToSpeak(
widget.text,
context: context,
targetID: 'text-audio-button-${widget.uniqueID}',
langCode: l2,
);
}
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"text": widget.text,
},
);
} finally {
if (mounted) {
setState(() => _isPlaying = false);
}
}
}
},
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
child: Text(
widget.text,
style: widget.style ?? Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
),
if (_isLoading)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 3,
),
)
else
Icon(
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
color:
_isPlaying ? Theme.of(context).colorScheme.primary : null,
size: widget.iconSize,
),
],
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
child: Text(
text,
style: style ?? Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
),
),
WordAudioButton(
text: text,
uniqueID: uniqueID,
isSelected: false,
baseOpacity: 1,
langCode: langCode,
padding: const EdgeInsets.only(left: 8.0),
),
],
);
}
}

View file

@ -9,7 +9,6 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_unsubscribed_card.dart';
@ -38,9 +37,6 @@ class ReadingAssistanceContent extends StatefulWidget {
}
class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
TtsController get ttsController =>
widget.overlayController.widget.chatController.choreographer.tts;
Widget? toolbarContent(BuildContext context) {
final bool? subscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
@ -123,7 +119,6 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
return WordZoomWidget(
token: widget.overlayController.selectedToken!,
messageEvent: widget.overlayController.pangeaMessageEvent!,
tts: ttsController,
overlayController: widget.overlayController,
);
}

View file

@ -8,7 +8,6 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@ -18,7 +17,6 @@ class LemmaWidget extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent;
final VoidCallback onEdit;
final VoidCallback onEditDone;
final TtsController tts;
final MessageOverlayController? overlayController;
const LemmaWidget({
@ -27,7 +25,6 @@ class LemmaWidget extends StatefulWidget {
required this.pangeaMessageEvent,
required this.onEdit,
required this.onEditDone,
required this.tts,
required this.overlayController,
});

View file

@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart';
import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
@ -22,14 +21,12 @@ import 'package:fluffychat/widgets/matrix.dart';
class WordZoomWidget extends StatelessWidget {
final PangeaToken token;
final PangeaMessageEvent messageEvent;
final TtsController tts;
final MessageOverlayController overlayController;
const WordZoomWidget({
super.key,
required this.token,
required this.messageEvent,
required this.tts,
required this.overlayController,
});
@ -93,7 +90,6 @@ class WordZoomWidget extends StatelessWidget {
debugPrint("what are we doing edits with?");
_onEditDone();
},
tts: tts,
overlayController: overlayController,
),
ConstructXpWidget(
@ -181,7 +177,7 @@ class WordZoomWidget extends StatelessWidget {
baseOpacity: 0.4,
uniqueID: "word-zoom-audio-${_selectedToken.text.content}",
langCode: overlayController
.pangeaMessageEvent?.messageDisplayLangCode,
.pangeaMessageEvent!.messageDisplayLangCode,
),
],
..._selectedToken.morphsBasicallyEligibleForPracticeByPriority

View file

@ -8,11 +8,11 @@ import 'package:punycode/punycode.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../widgets/adaptive_dialogs/public_room_dialog.dart';
import 'platform_infos.dart';
class UrlLauncher {
@ -188,9 +188,10 @@ class UrlLauncher {
// roomAlias: identityParts.primaryIdentifier,
// ),
// );
await PublicRoomDialog.show(
await PublicRoomBottomSheet.show(
context: context,
roomAlias: identityParts.primaryIdentifier,
// Pangea#
);
// Pangea#
}

View file

@ -6,7 +6,6 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import '../../config/themes.dart';
import '../../utils/url_launcher.dart';
@ -17,58 +16,12 @@ import '../matrix.dart';
import '../mxc_image_viewer.dart';
import 'adaptive_dialog_action.dart';
// #Pangea
// class PublicRoomDialog extends StatelessWidget {
class PublicRoomDialog extends StatefulWidget {
// Pangea#
class PublicRoomDialog extends StatelessWidget {
final String? roomAlias;
final PublicRoomsChunk? chunk;
final List<String>? via;
const PublicRoomDialog({super.key, this.roomAlias, this.chunk, this.via});
// #Pangea
static Future<bool?> show({
required BuildContext context,
String? roomAlias,
PublicRoomsChunk? chunk,
List<String>? via,
}) async {
final room = MatrixState.pangeaController.matrixState.client
.getRoomById(chunk!.roomId);
if (room != null && room.membership == Membership.join) {
context.go("/rooms?spaceId=${room.id}");
return null;
}
return showAdaptiveDialog(
context: context,
barrierDismissible: true,
builder: (context) => PublicRoomDialog(
roomAlias: roomAlias,
chunk: chunk,
via: via,
),
);
}
@override
State<PublicRoomDialog> createState() => PublicRoomDialogState();
}
class PublicRoomDialogState extends State<PublicRoomDialog> {
PublicRoomsChunk? get chunk => widget.chunk;
String? get roomAlias => widget.roomAlias;
List<String>? get via => widget.via;
final TextEditingController _codeController = TextEditingController();
@override
void dispose() {
_codeController.dispose();
super.dispose();
}
// Pangea#
void _joinRoom(BuildContext context) async {
final client = Matrix.of(context).client;
@ -87,16 +40,9 @@ class PublicRoomDialogState extends State<PublicRoomDialog> {
via: via,
);
// #Pangea
// if (!knock && client.getRoomById(roomId) == null) {
// await client.waitForRoomInSync(roomId);
// }
final room = client.getRoomById(roomId);
if (!knock && (room == null || room.membership != Membership.join)) {
await client.waitForRoomInSync(roomId, join: true);
if (!knock && client.getRoomById(roomId) == null) {
await client.waitForRoomInSync(roomId);
}
// Pangea#
return roomId;
},
);
@ -119,11 +65,6 @@ class PublicRoomDialogState extends State<PublicRoomDialog> {
!client.getRoomById(result.result!)!.isSpace) {
context.go('/rooms/$roomId');
}
// #Pangea
else {
context.go('/rooms?spaceId=$roomId');
}
// Pangea#
return;
}
@ -144,31 +85,10 @@ class PublicRoomDialogState extends State<PublicRoomDialog> {
return query.chunk.firstWhere(_testRoom);
}
// #Pangea
Future<void> _joinWithCode() async {
final resp =
await MatrixState.pangeaController.classController.joinClasswithCode(
context,
_codeController.text,
notFoundError: L10n.of(context).notTheCodeError,
);
if (!resp.isError) {
Navigator.of(context).pop(true);
}
}
// Pangea#
@override
Widget build(BuildContext context) {
final roomAlias = this.roomAlias ?? chunk?.canonicalAlias;
// #Pangea
// final roomLink = roomAlias ?? chunk?.roomId;
String? roomLink = roomAlias ?? chunk?.roomId;
if (roomLink != null) {
roomLink =
"${Environment.frontendURL}/#/join_with_alias?alias=${Uri.encodeComponent(roomLink)}";
}
// Pangea#
final roomLink = roomAlias ?? chunk?.roomId;
var copied = false;
return AlertDialog.adaptive(
title: ConstrainedBox(
@ -179,13 +99,7 @@ class PublicRoomDialogState extends State<PublicRoomDialog> {
),
),
content: ConstrainedBox(
// #Pangea
// constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
constraints: const BoxConstraints(
maxWidth: 256,
maxHeight: 300,
),
// Pangea#
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
child: FutureBuilder<PublicRoomsChunk>(
future: _search(context),
builder: (context, snapshot) {
@ -208,10 +122,7 @@ class PublicRoomDialogState extends State<PublicRoomDialog> {
child: GestureDetector(
onTap: () {
Clipboard.setData(
// #Pangea
// ClipboardData(text: roomLink),
ClipboardData(text: roomLink!),
// Pangea#
ClipboardData(text: roomLink),
);
setState(() {
copied = true;
@ -243,12 +154,7 @@ class PublicRoomDialogState extends State<PublicRoomDialog> {
),
),
),
// #Pangea
// TextSpan(text: roomLink),
TextSpan(
text: L10n.of(context).shareSpaceLink,
),
// Pangea#
TextSpan(text: roomLink),
],
style: theme.textTheme.bodyMedium
?.copyWith(fontSize: 10),
@ -280,79 +186,6 @@ class PublicRoomDialogState extends State<PublicRoomDialog> {
style: const TextStyle(fontSize: 10),
textAlign: TextAlign.center,
),
// #Pangea
Material(
type: MaterialType.transparency,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(24),
),
child: Row(
children: [
Expanded(
child: TextField(
style: const TextStyle(
fontSize: 12,
),
controller: _codeController,
decoration: InputDecoration(
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: L10n.of(context).enterSpaceCode,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
labelStyle: const TextStyle(
fontSize: 12,
),
hintStyle: TextStyle(
color: Theme.of(context).hintColor,
fontSize: 12,
),
isDense: true,
),
),
),
Container(
decoration: BoxDecoration(
border: Border(
left: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
),
child: ElevatedButton(
onPressed: _joinWithCode,
style: ElevatedButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.zero,
bottomLeft: Radius.zero,
topRight: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
padding: EdgeInsets.zero,
),
child: Text(
L10n.of(context).join,
style: const TextStyle(
fontSize: 12,
),
),
),
),
],
),
),
),
// Pangea#
if (topic != null && topic.isNotEmpty)
SelectableLinkify(
text: topic,
@ -381,10 +214,7 @@ class PublicRoomDialogState extends State<PublicRoomDialog> {
child: Text(
chunk?.joinRule == 'knock' &&
Matrix.of(context).client.getRoomById(chunk!.roomId) == null
// #Pangea
// ? L10n.of(context).knock
? L10n.of(context).askToJoin
// Pangea#
? L10n.of(context).knock
: chunk?.roomType == 'm.space'
? L10n.of(context).joinSpace
: L10n.of(context).joinRoom,

View file

@ -20,6 +20,7 @@ import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
@ -249,6 +250,7 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
),
);
pangeaController = PangeaController(matrix: widget, matrixState: this);
TtsController.initialize();
// Pangea#
}
@ -482,6 +484,12 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
AppConfig.showPresences =
store.getBool(SettingKeys.showPresences) ?? AppConfig.showPresences;
// #Pangea
AppConfig.displayNavigationRail =
store.getBool(SettingKeys.displayNavigationRail) ??
AppConfig.displayNavigationRail;
// Pangea#
}
@override

View file

@ -34,6 +34,15 @@ class SpacesNavigationRail extends StatelessWidget {
.uri
.path
.startsWith('/rooms/settings');
// #Pangea
final isHomepage = GoRouter.of(context)
.routeInformationProvider
.value
.uri
.path
.contains('homepage');
final isColumnMode = FluffyThemes.isColumnMode(context);
// Pangea#
return StreamBuilder(
key: ValueKey(
client.userID.toString(),
@ -53,7 +62,12 @@ class SpacesNavigationRail extends StatelessWidget {
.toList();
return SizedBox(
width: FluffyThemes.navRailWidth,
// #Pangea
// width: FluffyThemes.navRailWidth,
width: isColumnMode
? FluffyThemes.navRailWidth
: FluffyThemes.navRailWidth * 0.75,
// Pangea#
child: Column(
children: [
Expanded(
@ -61,35 +75,56 @@ class SpacesNavigationRail extends StatelessWidget {
scrollDirection: Axis.vertical,
// #Pangea
// itemCount: rootSpaces.length + 2,
itemCount: rootSpaces.length + 3,
itemCount: rootSpaces.length + 4,
// Pangea#
itemBuilder: (context, i) {
// #Pangea
if (i == 0) {
return NaviRailItem(
isSelected: activeSpaceId == null && !isSettings,
onTap: onGoToChats,
isSelected: isColumnMode
? activeSpaceId == null && !isSettings
: isHomepage,
onTap: () => isColumnMode
? onGoToChats()
: context.go("/rooms/homepage"),
icon: const Padding(
padding: EdgeInsets.all(10.0),
// #Pangea
// child: Icon(Icons.forum_outlined),
child: Icon(Icons.home_outlined),
// Pangea#
),
selectedIcon: const Padding(
padding: EdgeInsets.all(10.0),
// #Pangea
// child: Icon(Icons.forum),
child: Icon(Icons.home),
// Pangea#
),
// #Pangea
// toolTip: L10n.of(context).chats,
toolTip: L10n.of(context).home,
// Pangea#
unreadBadgeFilter: (room) => true,
);
}
i--;
// Pangea#
if (i == 0) {
return isColumnMode
? const SizedBox()
: NaviRailItem(
// #Pangea
// isSelected: activeSpaceId == null && !isSettings,
isSelected: activeSpaceId == null &&
!isSettings &&
!isHomepage,
// Pangea#
onTap: onGoToChats,
icon: const Padding(
padding: EdgeInsets.all(10.0),
child: Icon(Icons.forum_outlined),
),
selectedIcon: const Padding(
padding: EdgeInsets.all(10.0),
child: Icon(Icons.forum),
),
toolTip: L10n.of(context).chats,
unreadBadgeFilter: (room) => true,
);
}
i--;
if (i == rootSpaces.length) {
// #Pangea
return NaviRailItem(

View file

@ -6,7 +6,7 @@ description: Learn a language while texting your friends.
# Pangea#
publish_to: none
# On version bump also increase the build number for F-Droid
version: 4.1.10+1
version: 4.1.10+2
environment:
sdk: ">=3.0.0 <4.0.0"