resolve merge conflict
This commit is contained in:
commit
98539327b6
87 changed files with 3635 additions and 2653 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -708,7 +708,9 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
void _onRouteChanged() {
|
||||
stopMediaStream.add(null);
|
||||
if (!stopMediaStream.isClosed) {
|
||||
stopMediaStream.add(null);
|
||||
}
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
233
lib/pangea/activity_planner/activity_planner_builder.dart
Normal file
233
lib/pangea/activity_planner/activity_planner_builder.dart
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
627
lib/pangea/activity_suggestions/activity_room_selection.dart
Normal file
627
lib/pangea/activity_suggestions/activity_room_selection.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(", "),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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!,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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!,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 ?? "");
|
||||
}
|
||||
|
||||
|
|
|
|||
395
lib/pangea/public_spaces/public_room_bottom_sheet.dart
Normal file
395
lib/pangea/public_spaces/public_room_bottom_sheet.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue