merge main into analytics branch
This commit is contained in:
commit
14ba8bf47b
42 changed files with 623 additions and 937 deletions
|
|
@ -2303,6 +2303,12 @@
|
|||
"user": {}
|
||||
}
|
||||
},
|
||||
"invitedBy": "📩 Invited by {user}",
|
||||
"@invitedBy": {
|
||||
"placeholders": {
|
||||
"user": {}
|
||||
}
|
||||
},
|
||||
"youInvitedUser": "📩 You invited {user}",
|
||||
"@youInvitedUser": {
|
||||
"placeholders": {
|
||||
|
|
@ -4126,5 +4132,6 @@
|
|||
"errorTypes": "Error Types",
|
||||
"level": "Level",
|
||||
"canceledSend": "Canceled send",
|
||||
"morphsUsed": "Morphs Used"
|
||||
"morphsUsed": "Morphs Used",
|
||||
"translationChoicesBody": "Click and hold an option for a hint."
|
||||
}
|
||||
|
|
@ -63,13 +63,11 @@ abstract class AppConfig {
|
|||
path: '/krille-chan/fluffychat/issues/new',
|
||||
);
|
||||
// #Pangea
|
||||
// static bool renderHtml = true;
|
||||
static const bool enableSentry = true;
|
||||
static const String sentryDns =
|
||||
'https://8591d0d863b646feb4f3dda7e5dcab38@o256755.ingest.sentry.io/5243143';
|
||||
static bool renderHtml = false;
|
||||
// static bool renderHtml = true;
|
||||
// Pangea#
|
||||
static bool renderHtml = true;
|
||||
static bool hideRedactedEvents = false;
|
||||
static bool hideUnknownEvents = true;
|
||||
static bool hideUnimportantStateEvents = true;
|
||||
|
|
|
|||
|
|
@ -15,15 +15,12 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
|||
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/game_state_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/games/story_game/round_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/report_message.dart';
|
||||
|
|
@ -115,12 +112,9 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
with WidgetsBindingObserver {
|
||||
// #Pangea
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
|
||||
late Choreographer choreographer = Choreographer(pangeaController, this);
|
||||
|
||||
/// Model of the current story game round
|
||||
GameRoundModel? currentRound;
|
||||
// Pangea#
|
||||
|
||||
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
|
||||
|
||||
late Client sendingClient;
|
||||
|
|
@ -300,27 +294,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
/// Recursive function that sets the current round, waits for it to
|
||||
/// finish, sets it, etc. until the chat view is no longer mounted.
|
||||
void setRound() {
|
||||
currentRound?.dispose();
|
||||
currentRound = GameRoundModel(room: room);
|
||||
room.client.onRoomState.stream.firstWhere((update) {
|
||||
if (update.roomId != roomId) return false;
|
||||
if (update.state is! Event) return false;
|
||||
if ((update.state as Event).type != PangeaEventTypes.storyGame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final game = GameModel.fromJson((update.state as Event).content);
|
||||
return game.previousRoundEndTime != null;
|
||||
}).then((_) {
|
||||
if (mounted) setRound();
|
||||
});
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
scrollController.addListener(_updateScrollController);
|
||||
|
|
@ -336,7 +309,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
sendingClient = Matrix.of(context).client;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// #Pangea
|
||||
setRound();
|
||||
if (!mounted) return;
|
||||
Future.delayed(const Duration(seconds: 1), () async {
|
||||
if (!mounted) return;
|
||||
|
|
@ -561,7 +533,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
//#Pangea
|
||||
choreographer.stateListener.close();
|
||||
choreographer.dispose();
|
||||
currentRound?.dispose();
|
||||
//Pangea#
|
||||
super.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
|||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
|
|
@ -28,21 +27,7 @@ class ChatEventList extends StatelessWidget {
|
|||
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
|
||||
|
||||
final events = controller.timeline!.events
|
||||
.where(
|
||||
(event) =>
|
||||
event.isVisibleInGui
|
||||
// #Pangea
|
||||
&&
|
||||
// In story game, hide messages sent by non-bot users in previous round
|
||||
(event.type != EventTypes.Message ||
|
||||
event.senderId == BotName.byEnvironment ||
|
||||
controller.currentRound?.previousRoundEnd == null ||
|
||||
event.originServerTs.isAfter(
|
||||
controller.currentRound!.previousRoundEnd!,
|
||||
))
|
||||
// Pangea#
|
||||
,
|
||||
)
|
||||
.where((event) => event.isVisibleInGui)
|
||||
.toList();
|
||||
final animateInEventIndex = controller.animateInEventIndex;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:animations/animations.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pages/chat/input_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/input_bar_wrapper.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -34,7 +33,7 @@ class ChatInputRow extends StatelessWidget {
|
|||
controller.pangeaController.languageController.activeL2Model();
|
||||
|
||||
String hintText() {
|
||||
if (controller.choreographer.choreoMode == ChoreoMode.it) {
|
||||
if (controller.choreographer.itController.willOpen) {
|
||||
return L10n.of(context)!.buildTranslation;
|
||||
}
|
||||
return activel1 != null &&
|
||||
|
|
@ -322,10 +321,7 @@ class ChatInputRow extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
||||
// #Pangea
|
||||
// child: InputBar(
|
||||
child: InputBarWrapper(
|
||||
// Pangea#
|
||||
child: InputBar(
|
||||
room: controller.room,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
|
|||
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_event_list.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_input_row.dart';
|
||||
import 'package:fluffychat/pages/chat/pinned_events.dart';
|
||||
import 'package:fluffychat/pages/chat/reactions_picker.dart';
|
||||
import 'package:fluffychat/pages/chat/reply_display.dart';
|
||||
|
|
@ -14,7 +13,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
|
|||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/round_timer.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/input_bar_wrapper.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
|
|
@ -121,10 +120,6 @@ class ChatView extends StatelessWidget {
|
|||
// #Pangea
|
||||
} else {
|
||||
return [
|
||||
RoundTimer(controller: controller),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
ChatSettingsPopupMenu(
|
||||
controller.room,
|
||||
(!controller.room.isDirectChat && !controller.room.isArchived),
|
||||
|
|
@ -514,7 +509,9 @@ class ChatView extends StatelessWidget {
|
|||
),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatInputRowWrapper(
|
||||
controller: controller,
|
||||
),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
extension EventInfoDialogExtension on Event {
|
||||
void showInfoDialog(BuildContext context) => showAdaptiveBottomSheet(
|
||||
|
|
@ -47,16 +49,15 @@ class EventInfoDialog extends StatelessWidget {
|
|||
children: [
|
||||
ListTile(
|
||||
leading: Avatar(
|
||||
// mxContent: event.senderFromMemoryOrFallback.avatarUrl,
|
||||
// name: event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
name: "?",
|
||||
mxContent: event.senderFromMemoryOrFallback.avatarUrl,
|
||||
name: event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
client: event.room.client,
|
||||
// presenceUserId: event.senderId,
|
||||
presenceUserId: event.senderId,
|
||||
),
|
||||
title: Text(L10n.of(context)!.sender),
|
||||
// subtitle: Text(
|
||||
// '${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]',
|
||||
// ),
|
||||
subtitle: Text(
|
||||
'${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]',
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.time),
|
||||
|
|
|
|||
|
|
@ -263,10 +263,9 @@ class Message extends StatelessWidget {
|
|||
final user = snapshot.data ??
|
||||
event.senderFromMemoryOrFallback;
|
||||
return Avatar(
|
||||
// mxContent: user.avatarUrl,
|
||||
// name: user.calcDisplayname(),
|
||||
// presenceUserId: user.stateKey,
|
||||
name: "?",
|
||||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
presenceUserId: user.stateKey,
|
||||
presenceBackgroundColor:
|
||||
avatarPresenceBackgroundColor,
|
||||
onTap: () => onAvatarTab(event),
|
||||
|
|
@ -289,11 +288,10 @@ class Message extends StatelessWidget {
|
|||
: FutureBuilder<User?>(
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
// final displayname = snapshot.data
|
||||
// ?.calcDisplayname() ??
|
||||
// event.senderFromMemoryOrFallback
|
||||
// .calcDisplayname();
|
||||
const displayname = "?";
|
||||
final displayname = snapshot.data
|
||||
?.calcDisplayname() ??
|
||||
event.senderFromMemoryOrFallback
|
||||
.calcDisplayname();
|
||||
return Text(
|
||||
displayname,
|
||||
style: TextStyle(
|
||||
|
|
|
|||
|
|
@ -96,16 +96,12 @@ class MessageContent extends StatelessWidget {
|
|||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Avatar(
|
||||
// mxContent: sender.avatarUrl,
|
||||
// name: sender.calcDisplayname(),
|
||||
// presenceUserId: sender.stateKey,
|
||||
name: "?",
|
||||
mxContent: sender.avatarUrl,
|
||||
name: sender.calcDisplayname(),
|
||||
presenceUserId: sender.stateKey,
|
||||
client: event.room.client,
|
||||
),
|
||||
title: const Text(
|
||||
// sender.calcDisplayname(),
|
||||
"?",
|
||||
),
|
||||
title: Text(sender.calcDisplayname()),
|
||||
subtitle: Text(event.originServerTs.localizedTime(context)),
|
||||
trailing: const Icon(Icons.lock_outlined),
|
||||
),
|
||||
|
|
@ -269,10 +265,9 @@ class MessageContent extends StatelessWidget {
|
|||
builder: (context, snapshot) {
|
||||
final reason =
|
||||
event.redactedBecause?.content.tryGet<String>('reason');
|
||||
// final redactedBy = snapshot.data?.calcDisplayname() ??
|
||||
// event.redactedBecause?.senderId.localpart ??
|
||||
// L10n.of(context)!.user;
|
||||
const redactedBy = "?";
|
||||
final redactedBy = snapshot.data?.calcDisplayname() ??
|
||||
event.redactedBecause?.senderId.localpart ??
|
||||
L10n.of(context)!.user;
|
||||
return _ButtonContent(
|
||||
label: reason == null
|
||||
? L10n.of(context)!.redactedBy(redactedBy)
|
||||
|
|
@ -390,9 +385,8 @@ class MessageContent extends StatelessWidget {
|
|||
builder: (context, snapshot) {
|
||||
return _ButtonContent(
|
||||
label: L10n.of(context)!.userSentUnknownEvent(
|
||||
// snapshot.data?.calcDisplayname() ??
|
||||
// event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
"?",
|
||||
snapshot.data?.calcDisplayname() ??
|
||||
event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
event.type,
|
||||
),
|
||||
icon: 'ℹ️',
|
||||
|
|
|
|||
|
|
@ -59,8 +59,7 @@ class ReplyContent extends StatelessWidget {
|
|||
future: displayEvent.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
// '${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:',
|
||||
'?:',
|
||||
'${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import 'package:emojis/emoji.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/command_hints.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
|
|
@ -225,106 +229,117 @@ class InputBar extends StatelessWidget {
|
|||
Map<String, String?> suggestion,
|
||||
Client? client,
|
||||
) {
|
||||
const size = 30.0;
|
||||
// #Pangea
|
||||
// const size = 30.0;
|
||||
// const padding = EdgeInsets.all(4.0);
|
||||
// if (suggestion['type'] == 'command') {
|
||||
// final command = suggestion['name']!;
|
||||
// final hint = commandHint(L10n.of(context)!, command);
|
||||
// return Tooltip(
|
||||
// message: hint,
|
||||
// waitDuration: const Duration(days: 1), // don't show on hover
|
||||
// child: Container(
|
||||
// padding: padding,
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(
|
||||
// commandExample(command),
|
||||
// style: const TextStyle(fontFamily: 'monospace'),
|
||||
// ),
|
||||
// Text(
|
||||
// hint,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// style: Theme.of(context).textTheme.bodySmall,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// if (suggestion['type'] == 'emoji') {
|
||||
// final label = suggestion['label']!;
|
||||
// return Tooltip(
|
||||
// message: label,
|
||||
// waitDuration: const Duration(days: 1), // don't show on hover
|
||||
// child: Container(
|
||||
// padding: padding,
|
||||
// child: Text(label, style: const TextStyle(fontFamily: 'monospace')),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// if (suggestion['type'] == 'emote') {
|
||||
// return Container(
|
||||
// padding: padding,
|
||||
// child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// children: <Widget>[
|
||||
// MxcImage(
|
||||
// // ensure proper ordering ...
|
||||
// key: ValueKey(suggestion['name']),
|
||||
// uri: suggestion['mxc'] is String
|
||||
// ? Uri.parse(suggestion['mxc'] ?? '')
|
||||
// : null,
|
||||
// width: size,
|
||||
// height: size,
|
||||
// ),
|
||||
// const SizedBox(width: 6),
|
||||
// Text(suggestion['name']!),
|
||||
// Expanded(
|
||||
// child: Align(
|
||||
// alignment: Alignment.centerRight,
|
||||
// child: Opacity(
|
||||
// opacity: suggestion['pack_avatar_url'] != null ? 0.8 : 0.5,
|
||||
// child: suggestion['pack_avatar_url'] != null
|
||||
// ? Avatar(
|
||||
// mxContent: Uri.tryParse(
|
||||
// suggestion.tryGet<String>('pack_avatar_url') ?? '',
|
||||
// ),
|
||||
// name: suggestion.tryGet<String>('pack_display_name'),
|
||||
// size: size * 0.9,
|
||||
// client: client,
|
||||
// )
|
||||
// : Text(suggestion['pack_display_name']!),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// if (suggestion['type'] == 'user' || suggestion['type'] == 'room') {
|
||||
// final url = Uri.parse(suggestion['avatar_url'] ?? '');
|
||||
// return Container(
|
||||
// padding: padding,
|
||||
// child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// children: <Widget>[
|
||||
// Avatar(
|
||||
// mxContent: url,
|
||||
// name: suggestion.tryGet<String>('displayname') ??
|
||||
// suggestion.tryGet<String>('mxid'),
|
||||
// size: size,
|
||||
// client: client,
|
||||
// ),
|
||||
// const SizedBox(width: 6),
|
||||
// Text(suggestion['displayname'] ?? suggestion['mxid']!),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
const padding = EdgeInsets.all(8.0);
|
||||
// Pangea#
|
||||
if (suggestion['type'] == 'command') {
|
||||
final command = suggestion['name']!;
|
||||
final hint = commandHint(L10n.of(context)!, command);
|
||||
return Tooltip(
|
||||
message: hint,
|
||||
waitDuration: const Duration(days: 1), // don't show on hover
|
||||
child: Container(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
commandExample(command),
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
Text(
|
||||
hint,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (suggestion['type'] == 'emoji') {
|
||||
final label = suggestion['label']!;
|
||||
return Tooltip(
|
||||
message: label,
|
||||
waitDuration: const Duration(days: 1), // don't show on hover
|
||||
child: Container(
|
||||
padding: padding,
|
||||
child: Text(label, style: const TextStyle(fontFamily: 'monospace')),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (suggestion['type'] == 'emote') {
|
||||
return Container(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
MxcImage(
|
||||
// ensure proper ordering ...
|
||||
key: ValueKey(suggestion['name']),
|
||||
uri: suggestion['mxc'] is String
|
||||
? Uri.parse(suggestion['mxc'] ?? '')
|
||||
: null,
|
||||
width: size,
|
||||
height: size,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(suggestion['name']!),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Opacity(
|
||||
opacity: suggestion['pack_avatar_url'] != null ? 0.8 : 0.5,
|
||||
child: suggestion['pack_avatar_url'] != null
|
||||
? Avatar(
|
||||
mxContent: Uri.tryParse(
|
||||
suggestion.tryGet<String>('pack_avatar_url') ?? '',
|
||||
),
|
||||
name: suggestion.tryGet<String>('pack_display_name'),
|
||||
size: size * 0.9,
|
||||
client: client,
|
||||
)
|
||||
: Text(suggestion['pack_display_name']!),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (suggestion['type'] == 'user' || suggestion['type'] == 'room') {
|
||||
final url = Uri.parse(suggestion['avatar_url'] ?? '');
|
||||
return Container(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Avatar(
|
||||
mxContent: url,
|
||||
name: suggestion.tryGet<String>('displayname') ??
|
||||
suggestion.tryGet<String>('mxid'),
|
||||
size: size,
|
||||
client: client,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// #Pangea
|
||||
Flexible(
|
||||
child:
|
||||
// Pangea#
|
||||
Text(
|
||||
suggestion['displayname'] ?? suggestion['mxid']!,
|
||||
// #Pangea
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
|
|
@ -533,6 +548,15 @@ class InputBar extends StatelessWidget {
|
|||
// fix loading briefly flickering a dark box
|
||||
emptyBuilder: (BuildContext context) => const SizedBox
|
||||
.shrink(), // fix loading briefly showing no suggestions
|
||||
// #Pangea
|
||||
// If we ever want to change the suggestion background color
|
||||
// here is the code for it
|
||||
// decorationBuilder: (context, child) => Material(
|
||||
// borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
// color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
// child: child,
|
||||
// ),
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -37,10 +37,9 @@ class SeenByRow extends StatelessWidget {
|
|||
? seenByUsers.sublist(0, maxAvatars)
|
||||
: seenByUsers)
|
||||
.map(
|
||||
(user) => const Avatar(
|
||||
// mxContent: user.avatarUrl,
|
||||
// name: user.calcDisplayname(),
|
||||
name: "?",
|
||||
(user) => Avatar(
|
||||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TypingIndicators extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
|
@ -55,11 +56,10 @@ class TypingIndicators extends StatelessWidget {
|
|||
child: Stack(
|
||||
children: [
|
||||
if (typingUsers.isNotEmpty)
|
||||
const Avatar(
|
||||
Avatar(
|
||||
size: avatarSize,
|
||||
// mxContent: typingUsers.first.avatarUrl,
|
||||
// name: typingUsers.first.calcDisplayname(),
|
||||
name: "?",
|
||||
mxContent: typingUsers.first.avatarUrl,
|
||||
name: typingUsers.first.calcDisplayname(),
|
||||
),
|
||||
if (typingUsers.length == 2)
|
||||
Padding(
|
||||
|
|
@ -69,10 +69,9 @@ class TypingIndicators extends StatelessWidget {
|
|||
mxContent: typingUsers.length == 2
|
||||
? typingUsers.last.avatarUrl
|
||||
: null,
|
||||
// name: typingUsers.length == 2
|
||||
// ? typingUsers.last.calcDisplayname()
|
||||
// : '+${typingUsers.length - 1}',
|
||||
name: "?",
|
||||
name: typingUsers.length == 2
|
||||
? typingUsers.last.calcDisplayname()
|
||||
: '+${typingUsers.length - 1}',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -39,10 +39,9 @@ class ParticipantListItem extends StatelessWidget {
|
|||
),
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: Text(
|
||||
// user.calcDisplayname(),
|
||||
"?",
|
||||
user.calcDisplayname(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
@ -89,9 +88,8 @@ class ParticipantListItem extends StatelessWidget {
|
|||
subtitle: Text(user.id),
|
||||
leading: Avatar(
|
||||
mxContent: user.avatarUrl,
|
||||
// name: user.calcDisplayname(),
|
||||
// presenceUserId: user.stateKey,
|
||||
name: "?",
|
||||
name: user.calcDisplayname(),
|
||||
presenceUserId: user.stateKey,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/get_chat_list_item_subtitle.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/room_status_extension.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
|
|
@ -238,52 +240,51 @@ class ChatListItem extends StatelessWidget {
|
|||
softWrap: false,
|
||||
)
|
||||
// #Pangea
|
||||
: const SizedBox(),
|
||||
// FutureBuilder<String>(
|
||||
// future: room.lastEvent != null
|
||||
// ? GetChatListItemSubtitle().getSubtitle(
|
||||
// L10n.of(context)!,
|
||||
// room.lastEvent,
|
||||
// MatrixState.pangeaController,
|
||||
// )
|
||||
// : Future.value(L10n.of(context)!.emptyChat),
|
||||
// builder: (context, snapshot) {
|
||||
// // Pangea#
|
||||
// return Text(
|
||||
// room.membership == Membership.invite
|
||||
// ? isDirectChat
|
||||
// ? L10n.of(context)!.invitePrivateChat
|
||||
// : L10n.of(context)!.inviteGroupChat
|
||||
// // #Pangea
|
||||
// : snapshot.data ??
|
||||
// // Pangea#
|
||||
// room.lastEvent
|
||||
// ?.calcLocalizedBodyFallback(
|
||||
// MatrixLocals(L10n.of(context)!),
|
||||
// hideReply: true,
|
||||
// hideEdit: true,
|
||||
// plaintextBody: true,
|
||||
// removeMarkdown: true,
|
||||
// withSenderNamePrefix: !isDirectChat ||
|
||||
// directChatMatrixId !=
|
||||
// room.lastEvent?.senderId,
|
||||
// ) ??
|
||||
// L10n.of(context)!.emptyChat,
|
||||
// softWrap: false,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// style: TextStyle(
|
||||
// fontWeight: unread || room.hasNewMessages
|
||||
// ? FontWeight.bold
|
||||
// : null,
|
||||
// color: theme.colorScheme.onSurfaceVariant,
|
||||
// decoration: room.lastEvent?.redacted == true
|
||||
// ? TextDecoration.lineThrough
|
||||
// : null,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
: FutureBuilder<String>(
|
||||
future: room.lastEvent != null
|
||||
? GetChatListItemSubtitle().getSubtitle(
|
||||
L10n.of(context)!,
|
||||
room.lastEvent,
|
||||
MatrixState.pangeaController,
|
||||
)
|
||||
: Future.value(L10n.of(context)!.emptyChat),
|
||||
builder: (context, snapshot) {
|
||||
// Pangea#
|
||||
return Text(
|
||||
room.membership == Membership.invite
|
||||
? isDirectChat
|
||||
? L10n.of(context)!.invitePrivateChat
|
||||
: L10n.of(context)!.inviteGroupChat
|
||||
// #Pangea
|
||||
: snapshot.data ??
|
||||
// Pangea#
|
||||
room.lastEvent
|
||||
?.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true,
|
||||
hideEdit: true,
|
||||
plaintextBody: true,
|
||||
removeMarkdown: true,
|
||||
withSenderNamePrefix: !isDirectChat ||
|
||||
directChatMatrixId !=
|
||||
room.lastEvent?.senderId,
|
||||
) ??
|
||||
L10n.of(context)!.emptyChat,
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: unread || room.hasNewMessages
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
decoration: room.lastEvent?.redacted == true
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// #Pangea
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
||||
class ChatSearchMessageTab extends StatelessWidget {
|
||||
final String searchQuery;
|
||||
final Room room;
|
||||
|
|
@ -96,10 +98,9 @@ class ChatSearchMessageTab extends StatelessWidget {
|
|||
}
|
||||
final event = events[i];
|
||||
final sender = event.senderFromMemoryOrFallback;
|
||||
// final displayname = sender.calcDisplayname(
|
||||
// i18n: MatrixLocals(L10n.of(context)!),
|
||||
// );
|
||||
const displayname = "?";
|
||||
final displayname = sender.calcDisplayname(
|
||||
i18n: MatrixLocals(L10n.of(context)!),
|
||||
);
|
||||
return _MessageSearchResultListTile(
|
||||
sender: sender,
|
||||
displayname: displayname,
|
||||
|
|
|
|||
|
|
@ -22,10 +22,9 @@ class UserBottomSheetView extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final user = controller.widget.user;
|
||||
final userId = (user?.id ?? controller.widget.profile?.userId)!;
|
||||
// final displayname = (user?.calcDisplayname() ??
|
||||
// controller.widget.profile?.displayName ??
|
||||
// controller.widget.profile?.userId.localpart)!;
|
||||
const displayname = "?";
|
||||
final displayname = (user?.calcDisplayname() ??
|
||||
controller.widget.profile?.displayName ??
|
||||
controller.widget.profile?.userId.localpart)!;
|
||||
final avatarUrl = user?.avatarUrl ?? controller.widget.profile?.avatarUrl;
|
||||
|
||||
final client = Matrix.of(controller.widget.outerContext).client;
|
||||
|
|
@ -40,7 +39,7 @@ class UserBottomSheetView extends StatelessWidget {
|
|||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(displayname),
|
||||
Text(displayname),
|
||||
PresenceBuilder(
|
||||
userId: userId,
|
||||
client: client,
|
||||
|
|
@ -214,7 +213,7 @@ class UserBottomSheetView extends StatelessWidget {
|
|||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
label: const Text(
|
||||
label: Text(
|
||||
displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import 'dart:developer';
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/igc_text_data_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/igc_repo.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/span_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../models/span_card_model.dart';
|
||||
import '../../utils/error_handler.dart';
|
||||
|
|
@ -39,10 +41,12 @@ class IgcController {
|
|||
|
||||
final IGCRequestBody reqBody = IGCRequestBody(
|
||||
fullText: choreographer.currentText,
|
||||
userId: choreographer.pangeaController.userController.userId!,
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
enableIGC: choreographer.igcEnabled && !onlyTokensAndLanguageDetection,
|
||||
enableIT: choreographer.itEnabled && !onlyTokensAndLanguageDetection,
|
||||
prevMessages: prevMessages(),
|
||||
);
|
||||
|
||||
final IGCTextData igcTextDataResponse = await IgcRepo.getIGC(
|
||||
|
|
@ -125,6 +129,49 @@ class IgcController {
|
|||
);
|
||||
}
|
||||
|
||||
/// Get the content of previous text and audio messages in chat.
|
||||
/// Passed to IGC request to add context.
|
||||
List<PreviousMessage> prevMessages({int numMessages = 5}) {
|
||||
final List<Event> events = choreographer.chatController.visibleEvents
|
||||
.where(
|
||||
(e) =>
|
||||
e.type == EventTypes.Message &&
|
||||
(e.messageType == MessageTypes.Text ||
|
||||
e.messageType == MessageTypes.Audio),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final List<PreviousMessage> messages = [];
|
||||
for (final Event event in events) {
|
||||
final String? content = event.messageType == MessageTypes.Text
|
||||
? event.content.toString()
|
||||
: PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: choreographer.chatController.timeline!,
|
||||
ownMessage: event.senderId ==
|
||||
choreographer.pangeaController.matrixState.client.userID,
|
||||
)
|
||||
.getSpeechToTextLocal(
|
||||
choreographer.l1LangCode,
|
||||
choreographer.l2LangCode,
|
||||
)
|
||||
?.transcript
|
||||
.text;
|
||||
if (content == null) continue;
|
||||
messages.add(
|
||||
PreviousMessage(
|
||||
content: content,
|
||||
sender: event.senderId,
|
||||
timestamp: event.originServerTs,
|
||||
),
|
||||
);
|
||||
if (messages.length >= numMessages) {
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
bool get hasRelevantIGCTextData {
|
||||
if (igcTextData == null) return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:developer';
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -56,6 +57,23 @@ class ITController {
|
|||
choreographer.setState();
|
||||
}
|
||||
|
||||
bool _closingHint = false;
|
||||
Duration get animationSpeed => (_closingHint || !_willOpen)
|
||||
? const Duration(milliseconds: 500)
|
||||
: const Duration(milliseconds: 2000);
|
||||
|
||||
void closeHint() {
|
||||
_closingHint = true;
|
||||
final String hintKey = InlineInstructions.translationChoices.toString();
|
||||
final instructionsController = choreographer.pangeaController.instructions;
|
||||
instructionsController.turnOffInstruction(hintKey);
|
||||
instructionsController.updateEnableInstructions(hintKey, true);
|
||||
choreographer.setState();
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
_closingHint = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initializeIT(ITStartData itStartData) async {
|
||||
_willOpen = true;
|
||||
Future.delayed(const Duration(microseconds: 100), () {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -47,12 +49,16 @@ class ITBarState extends State<ITBar> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
bool get instructionsTurnedOff =>
|
||||
widget.choreographer.pangeaController.instructions
|
||||
.wereInstructionsTurnedOff(
|
||||
InlineInstructions.translationChoices.toString(),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSize(
|
||||
duration: itController.willOpen
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
duration: itController.animationSpeed,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
clipBehavior: Clip.none,
|
||||
child: !itController.willOpen
|
||||
|
|
@ -60,9 +66,7 @@ class ITBarState extends State<ITBar> {
|
|||
: CompositedTransformTarget(
|
||||
link: widget.choreographer.itBarLinkAndKey.link,
|
||||
child: AnimatedOpacity(
|
||||
duration: itController.willOpen
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
duration: itController.animationSpeed,
|
||||
opacity: itController.willOpen ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
key: widget.choreographer.itBarLinkAndKey.key,
|
||||
|
|
@ -116,6 +120,12 @@ class ITBarState extends State<ITBar> {
|
|||
// const SizedBox(height: 40.0),
|
||||
OriginalText(controller: itController),
|
||||
const SizedBox(height: 7.0),
|
||||
if (!instructionsTurnedOff)
|
||||
InlineTooltip(
|
||||
body: InlineInstructions.translationChoices
|
||||
.body(context),
|
||||
onClose: itController.closeHint,
|
||||
),
|
||||
IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints:
|
||||
|
|
@ -158,6 +168,7 @@ class ITBarState extends State<ITBar> {
|
|||
),
|
||||
),
|
||||
),
|
||||
// ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -312,7 +323,11 @@ class ITChoices extends StatelessWidget {
|
|||
chosenContinuance:
|
||||
controller.currentITStep!.continuances[index].text,
|
||||
bestContinuance: controller.currentITStep!.best.text,
|
||||
feedbackLang: controller.targetLangCode,
|
||||
// TODO: we want this to eventually switch between target and source lang,
|
||||
// based on the learner's proficiency - maybe with the words involved in the translation
|
||||
// maybe overall. For now, we'll just use the source lang.
|
||||
feedbackLang: controller.choreographer.l1Lang?.langCode ??
|
||||
controller.sourceLangCode,
|
||||
sourceTextLang: controller.sourceLangCode,
|
||||
targetLang: controller.targetLangCode,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -146,8 +146,11 @@ class ITFeedbackCardView extends StatelessWidget {
|
|||
controller.res!.text,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
// if res is not null, show a button to translate the text
|
||||
if (controller.res != null && controller.translatedFeedback == null)
|
||||
// if res is not null and feedback not in the userL1, show a button to translate the text
|
||||
if (controller.res != null &&
|
||||
controller.translatedFeedback == null &&
|
||||
controller.widget.req.feedbackLang !=
|
||||
controller.controller.languageController.userL1?.langCode)
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
class GameConstants {
|
||||
static const int timerMaxSeconds = 120;
|
||||
}
|
||||
|
|
@ -69,6 +69,10 @@ class ModelKey {
|
|||
static const String permissions = "permissions";
|
||||
static const String enableIGC = "enable_igc";
|
||||
static const String enableIT = "enable_it";
|
||||
static const String prevMessages = "prev_messages";
|
||||
static const String prevContent = "prev_content";
|
||||
static const String prevSender = "prev_sender";
|
||||
static const String prevTimestamp = "prev_timestamp";
|
||||
|
||||
static const String originalSent = "original_sent";
|
||||
static const String originalWritten = "original_written";
|
||||
|
|
@ -119,8 +123,4 @@ class ModelKey {
|
|||
|
||||
static const String prevEventId = "prev_event_id";
|
||||
static const String prevLastUpdated = "prev_last_updated";
|
||||
|
||||
static const String gameState = "game_state";
|
||||
static const String currentRoundStartTime = "start_time";
|
||||
static const String previousRoundEndTime = "message_visible_from";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,4 @@ class PangeaEventTypes {
|
|||
/// A record of completion of an activity. There
|
||||
/// can be one per user per activity.
|
||||
static const activityRecord = "pangea.activity_completion";
|
||||
|
||||
static const storyGame = "p.game.story";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
enum InlineInstructions {
|
||||
speechToText,
|
||||
l1Translation,
|
||||
translationChoices,
|
||||
}
|
||||
|
||||
extension InlineInstructionsExtension on InlineInstructions {
|
||||
|
|
@ -67,6 +68,21 @@ extension InlineInstructionsExtension on InlineInstructions {
|
|||
return L10n.of(context)!.speechToTextBody;
|
||||
case InlineInstructions.l1Translation:
|
||||
return L10n.of(context)!.l1TranslationBody;
|
||||
case InlineInstructions.translationChoices:
|
||||
return L10n.of(context)!.translationChoicesBody;
|
||||
}
|
||||
}
|
||||
|
||||
bool get toggledOff {
|
||||
final instructionSettings =
|
||||
MatrixState.pangeaController.userController.profile.instructionSettings;
|
||||
switch (this) {
|
||||
case InlineInstructions.speechToText:
|
||||
return instructionSettings.showedSpeechToTextTooltip;
|
||||
case InlineInstructions.l1Translation:
|
||||
return instructionSettings.showedL1TranslationTooltip;
|
||||
case InlineInstructions.translationChoices:
|
||||
return instructionSettings.showedTranslationChoicesTooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,85 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
extension MembershipUpdate on SyncUpdate {
|
||||
List<Event> messages(Room chat) {
|
||||
if (rooms?.join == null ||
|
||||
!rooms!.join!.containsKey(chat.id) ||
|
||||
rooms!.join![chat.id]!.timeline?.events == null) {
|
||||
return [];
|
||||
bool isMembershipUpdate(String userId) {
|
||||
return isMembershipUpdateByType(Membership.join, userId) ||
|
||||
isMembershipUpdateByType(Membership.leave, userId) ||
|
||||
isMembershipUpdateByType(Membership.invite, userId);
|
||||
}
|
||||
|
||||
bool isMembershipUpdateByType(Membership type, String userId) {
|
||||
final List<SyncRoomUpdate>? updates = getRoomUpdates(type);
|
||||
if (updates?.isEmpty ?? true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return rooms!.join![chat.id]!.timeline!.events!
|
||||
.where(
|
||||
(event) =>
|
||||
event.type == EventTypes.Message &&
|
||||
!event.eventId.startsWith("Pangea Chat"),
|
||||
)
|
||||
.map((event) => Event.fromMatrixEvent(event, chat))
|
||||
.toList();
|
||||
for (final SyncRoomUpdate update in updates!) {
|
||||
final List<dynamic>? events = getRoomUpdateEvents(type, update);
|
||||
if (hasMembershipUpdate(
|
||||
events,
|
||||
type.name,
|
||||
userId,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<SyncRoomUpdate>? getRoomUpdates(Membership type) {
|
||||
switch (type) {
|
||||
case Membership.join:
|
||||
return rooms?.join?.values.toList();
|
||||
case Membership.leave:
|
||||
return rooms?.leave?.values.toList();
|
||||
case Membership.invite:
|
||||
return rooms?.invite?.values.toList();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool isSpaceChildUpdate(String activeSpaceId) {
|
||||
if (rooms?.join?.isEmpty ?? true) {
|
||||
return false;
|
||||
}
|
||||
for (final update in rooms!.join!.entries) {
|
||||
final String spaceId = update.key;
|
||||
final List<MatrixEvent>? timelineEvents = update.value.timeline?.events;
|
||||
final bool isUpdate = timelineEvents != null &&
|
||||
spaceId == activeSpaceId &&
|
||||
timelineEvents.any((event) => event.type == EventTypes.SpaceChild);
|
||||
if (isUpdate) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<dynamic>? getRoomUpdateEvents(Membership type, SyncRoomUpdate update) {
|
||||
switch (type) {
|
||||
case Membership.join:
|
||||
return (update as JoinedRoomUpdate).timeline?.events;
|
||||
case Membership.leave:
|
||||
return (update as LeftRoomUpdate).timeline?.events;
|
||||
case Membership.invite:
|
||||
return (update as InvitedRoomUpdate).inviteState;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool hasMembershipUpdate(
|
||||
List<dynamic>? events,
|
||||
String membershipType,
|
||||
String userId,
|
||||
) {
|
||||
if (events == null) {
|
||||
return false;
|
||||
}
|
||||
return events.any(
|
||||
(event) =>
|
||||
event.type == EventTypes.RoomMember &&
|
||||
event.stateKey == userId &&
|
||||
event.content['membership'] == membershipType,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -264,6 +264,21 @@ class PangeaMessageEvent {
|
|||
null;
|
||||
}).toSet();
|
||||
|
||||
SpeechToTextModel? getSpeechToTextLocal(
|
||||
String? l1Code,
|
||||
String? l2Code,
|
||||
) {
|
||||
if (l1Code == null || l2Code == null) {
|
||||
return null;
|
||||
}
|
||||
return representations
|
||||
.firstWhereOrNull(
|
||||
(element) => element.content.speechToText != null,
|
||||
)
|
||||
?.content
|
||||
.speechToText;
|
||||
}
|
||||
|
||||
Future<SpeechToTextModel?> getSpeechToText(
|
||||
String l1Code,
|
||||
String l2Code,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -95,7 +96,8 @@ class OneConstructUse {
|
|||
factory OneConstructUse.fromJson(Map<String, dynamic> json) {
|
||||
return OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.values
|
||||
.firstWhere((e) => e.string == json['useType']),
|
||||
.firstWhereOrNull((e) => e.string == json['useType']) ??
|
||||
ConstructUseTypeEnum.unk,
|
||||
lemma: json['lemma'],
|
||||
form: json['form'],
|
||||
constructType: json['constructType'] != null
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix_api_lite/generated/model.dart';
|
||||
|
||||
class GameModel {
|
||||
DateTime? currentRoundStartTime;
|
||||
DateTime? previousRoundEndTime;
|
||||
|
||||
GameModel({
|
||||
this.currentRoundStartTime,
|
||||
this.previousRoundEndTime,
|
||||
});
|
||||
|
||||
factory GameModel.fromJson(json) {
|
||||
return GameModel(
|
||||
currentRoundStartTime: json[ModelKey.currentRoundStartTime] != null
|
||||
? DateTime.parse(json[ModelKey.currentRoundStartTime])
|
||||
: null,
|
||||
previousRoundEndTime: json[ModelKey.previousRoundEndTime] != null
|
||||
? DateTime.parse(json[ModelKey.previousRoundEndTime])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
try {
|
||||
data[ModelKey.currentRoundStartTime] =
|
||||
currentRoundStartTime?.toIso8601String();
|
||||
data[ModelKey.previousRoundEndTime] =
|
||||
previousRoundEndTime?.toIso8601String();
|
||||
return data;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
StateEvent get toStateEvent => StateEvent(
|
||||
content: toJson(),
|
||||
type: PangeaEventTypes.storyGame,
|
||||
);
|
||||
}
|
||||
|
|
@ -186,11 +186,18 @@ class UserInstructions {
|
|||
bool showedBlurMeansTranslate;
|
||||
bool showedTooltipInstructions;
|
||||
|
||||
bool showedSpeechToTextTooltip;
|
||||
bool showedL1TranslationTooltip;
|
||||
bool showedTranslationChoicesTooltip;
|
||||
|
||||
UserInstructions({
|
||||
this.showedItInstructions = false,
|
||||
this.showedClickMessage = false,
|
||||
this.showedBlurMeansTranslate = false,
|
||||
this.showedTooltipInstructions = false,
|
||||
this.showedSpeechToTextTooltip = false,
|
||||
this.showedL1TranslationTooltip = false,
|
||||
this.showedTranslationChoicesTooltip = false,
|
||||
});
|
||||
|
||||
factory UserInstructions.fromJson(Map<String, dynamic> json) =>
|
||||
|
|
@ -203,6 +210,12 @@ class UserInstructions {
|
|||
json[InstructionsEnum.blurMeansTranslate.toString()] ?? false,
|
||||
showedTooltipInstructions:
|
||||
json[InstructionsEnum.tooltipInstructions.toString()] ?? false,
|
||||
showedL1TranslationTooltip:
|
||||
json[InlineInstructions.l1Translation.toString()] ?? false,
|
||||
showedTranslationChoicesTooltip:
|
||||
json[InlineInstructions.translationChoices.toString()] ?? false,
|
||||
showedSpeechToTextTooltip:
|
||||
json[InlineInstructions.speechToText.toString()] ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -213,6 +226,12 @@ class UserInstructions {
|
|||
showedBlurMeansTranslate;
|
||||
data[InstructionsEnum.tooltipInstructions.toString()] =
|
||||
showedTooltipInstructions;
|
||||
data[InlineInstructions.l1Translation.toString()] =
|
||||
showedL1TranslationTooltip;
|
||||
data[InlineInstructions.translationChoices.toString()] =
|
||||
showedTranslationChoicesTooltip;
|
||||
data[InlineInstructions.speechToText.toString()] =
|
||||
showedSpeechToTextTooltip;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -238,6 +257,21 @@ class UserInstructions {
|
|||
?.content[InstructionsEnum.tooltipInstructions.toString()]
|
||||
as bool?) ??
|
||||
false,
|
||||
showedL1TranslationTooltip:
|
||||
(accountData[InlineInstructions.l1Translation.toString()]
|
||||
?.content[InlineInstructions.l1Translation.toString()]
|
||||
as bool?) ??
|
||||
false,
|
||||
showedTranslationChoicesTooltip: (accountData[
|
||||
InlineInstructions.translationChoices.toString()]
|
||||
?.content[InlineInstructions.translationChoices.toString()]
|
||||
as bool?) ??
|
||||
false,
|
||||
showedSpeechToTextTooltip:
|
||||
(accountData[InlineInstructions.speechToText.toString()]
|
||||
?.content[InlineInstructions.speechToText.toString()]
|
||||
as bool?) ??
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/game_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/sync_update_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/game_state_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// A model of a game round. Manages the round's state and duration.
|
||||
class GameRoundModel {
|
||||
final Duration roundDuration = const Duration(
|
||||
seconds: GameConstants.timerMaxSeconds,
|
||||
);
|
||||
|
||||
final Room room;
|
||||
|
||||
// All the below state variables are used for sending and managing
|
||||
// round start and end times. Once the bot starts doing that, they should be removed.
|
||||
late DateTime createdAt;
|
||||
Timer? timer;
|
||||
StreamSubscription? syncSubscription;
|
||||
final List<String> userMessageIDs = [];
|
||||
final List<String> botMessageIDs = [];
|
||||
|
||||
GameRoundModel({
|
||||
required this.room,
|
||||
}) {
|
||||
createdAt = DateTime.now();
|
||||
|
||||
// if, on creation, the current round is already ongoing,
|
||||
// start the timer (or reset it if the round went over)
|
||||
if (currentRoundStart != null) {
|
||||
final currentRoundDuration = DateTime.now().difference(
|
||||
currentRoundStart!,
|
||||
);
|
||||
final roundFinished = currentRoundDuration > roundDuration;
|
||||
|
||||
if (roundFinished) {
|
||||
endRound();
|
||||
}
|
||||
}
|
||||
|
||||
// listen to syncs for new bot messages to start and stop rounds
|
||||
syncSubscription ??= room.client.onSync.stream.listen(_handleSync);
|
||||
}
|
||||
|
||||
GameModel get gameState => GameModel.fromJson(
|
||||
room.getState(PangeaEventTypes.storyGame)?.content ?? {},
|
||||
);
|
||||
|
||||
DateTime? get currentRoundStart => gameState.currentRoundStartTime;
|
||||
DateTime? get previousRoundEnd => gameState.previousRoundEndTime;
|
||||
|
||||
void _handleSync(SyncUpdate update) {
|
||||
final newMessages = update
|
||||
.messages(room)
|
||||
.where((msg) => msg.originServerTs.isAfter(createdAt))
|
||||
.toList();
|
||||
|
||||
final botMessages = newMessages
|
||||
.where((msg) => msg.senderId == BotName.byEnvironment)
|
||||
.toList();
|
||||
final userMessages = newMessages
|
||||
.where((msg) => msg.senderId != BotName.byEnvironment)
|
||||
.toList();
|
||||
|
||||
final hasNewBotMessage = botMessages.any(
|
||||
(msg) => !botMessageIDs.contains(msg.eventId),
|
||||
);
|
||||
|
||||
if (hasNewBotMessage) {
|
||||
if (currentRoundStart == null) {
|
||||
startRound();
|
||||
} else {
|
||||
endRound();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRoundStart != null) {
|
||||
for (final message in botMessages) {
|
||||
if (!botMessageIDs.contains(message.eventId)) {
|
||||
botMessageIDs.add(message.eventId);
|
||||
}
|
||||
}
|
||||
|
||||
for (final message in userMessages) {
|
||||
if (!userMessageIDs.contains(message.eventId)) {
|
||||
userMessageIDs.add(message.eventId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the start and end times of the current and previous rounds.
|
||||
Future<void> setRoundTimes({
|
||||
DateTime? currentRoundStart,
|
||||
DateTime? previousRoundEnd,
|
||||
}) async {
|
||||
final game = GameModel.fromJson(
|
||||
room.getState(PangeaEventTypes.storyGame)?.content ?? {},
|
||||
);
|
||||
|
||||
game.currentRoundStartTime = currentRoundStart;
|
||||
game.previousRoundEndTime = previousRoundEnd;
|
||||
|
||||
await room.client.setRoomStateWithKey(
|
||||
room.id,
|
||||
PangeaEventTypes.storyGame,
|
||||
'',
|
||||
game.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Start a new round.
|
||||
void startRound() {
|
||||
setRoundTimes(
|
||||
currentRoundStart: DateTime.now(),
|
||||
previousRoundEnd: null,
|
||||
).then((_) => timer = Timer(roundDuration, endRound));
|
||||
}
|
||||
|
||||
/// End and cleanup after the current round.
|
||||
void endRound() {
|
||||
syncSubscription?.cancel();
|
||||
syncSubscription = null;
|
||||
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
|
||||
setRoundTimes(
|
||||
currentRoundStart: null,
|
||||
previousRoundEnd: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
syncSubscription?.cancel();
|
||||
syncSubscription = null;
|
||||
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -94,12 +94,43 @@ class IgcRepo {
|
|||
}
|
||||
}
|
||||
|
||||
/// Previous text/audio message sent in chat
|
||||
/// Contain message content, sender, and timestamp
|
||||
class PreviousMessage {
|
||||
String content;
|
||||
String sender;
|
||||
DateTime timestamp;
|
||||
|
||||
PreviousMessage({
|
||||
required this.content,
|
||||
required this.sender,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
factory PreviousMessage.fromJson(Map<String, dynamic> json) =>
|
||||
PreviousMessage(
|
||||
content: json[ModelKey.prevContent] ?? "",
|
||||
sender: json[ModelKey.prevSender] ?? "",
|
||||
timestamp: json[ModelKey.prevTimestamp] == null
|
||||
? DateTime.now()
|
||||
: DateTime.parse(json[ModelKey.prevTimestamp]),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
ModelKey.prevContent: content,
|
||||
ModelKey.prevSender: sender,
|
||||
ModelKey.prevTimestamp: timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
class IGCRequestBody {
|
||||
String fullText;
|
||||
String userL1;
|
||||
String userL2;
|
||||
bool enableIT;
|
||||
bool enableIGC;
|
||||
String userId;
|
||||
List<PreviousMessage> prevMessages;
|
||||
|
||||
IGCRequestBody({
|
||||
required this.fullText,
|
||||
|
|
@ -107,6 +138,8 @@ class IGCRequestBody {
|
|||
required this.userL2,
|
||||
required this.enableIGC,
|
||||
required this.enableIT,
|
||||
required this.userId,
|
||||
required this.prevMessages,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
|
|
@ -115,5 +148,8 @@ class IGCRequestBody {
|
|||
ModelKey.userL2: userL2,
|
||||
"enable_it": enableIT,
|
||||
"enable_igc": enableIGC,
|
||||
ModelKey.userId: userId,
|
||||
ModelKey.prevMessages:
|
||||
jsonEncode(prevMessages.map((x) => x.toJson()).toList()),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class InlineTooltip extends StatelessWidget {
|
|||
onPressed: onClose,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(20),
|
||||
|
|
|
|||
|
|
@ -25,9 +25,15 @@ class InstructionsController {
|
|||
final Map<String, bool> _instructionsShown = {};
|
||||
|
||||
/// Returns true if the user requested this popup not be shown again
|
||||
bool? toggledOff(String key) => InstructionsEnum.values
|
||||
.firstWhereOrNull((value) => value.toString() == key)
|
||||
?.toggledOff;
|
||||
bool? toggledOff(String key) {
|
||||
final bool? instruction = InstructionsEnum.values
|
||||
.firstWhereOrNull((value) => value.toString() == key)
|
||||
?.toggledOff;
|
||||
final bool? tooltip = InlineInstructions.values
|
||||
.firstWhereOrNull((value) => value.toString() == key)
|
||||
?.toggledOff;
|
||||
return instruction ?? tooltip;
|
||||
}
|
||||
|
||||
InstructionsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
|
|
@ -58,6 +64,15 @@ class InstructionsController {
|
|||
if (key == InstructionsEnum.tooltipInstructions.toString()) {
|
||||
profile.instructionSettings.showedTooltipInstructions = value;
|
||||
}
|
||||
if (key == InlineInstructions.speechToText.toString()) {
|
||||
profile.instructionSettings.showedSpeechToTextTooltip = value;
|
||||
}
|
||||
if (key == InlineInstructions.l1Translation.toString()) {
|
||||
profile.instructionSettings.showedL1TranslationTooltip = value;
|
||||
}
|
||||
if (key == InlineInstructions.translationChoices.toString()) {
|
||||
profile.instructionSettings.showedTranslationChoicesTooltip = value;
|
||||
}
|
||||
return profile;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,23 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fluffychat/pages/chat/input_bar.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_input_row.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class InputBarWrapper extends StatefulWidget {
|
||||
final Room room;
|
||||
final int? minLines;
|
||||
final int? maxLines;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final ValueChanged<Uint8List?>? onSubmitImage;
|
||||
final FocusNode? focusNode;
|
||||
final PangeaTextController? controller;
|
||||
final InputDecoration? decoration;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final bool? autofocus;
|
||||
final bool readOnly;
|
||||
class ChatInputRowWrapper extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
|
||||
const InputBarWrapper({
|
||||
required this.room,
|
||||
this.minLines,
|
||||
this.maxLines,
|
||||
this.keyboardType,
|
||||
this.onSubmitted,
|
||||
this.onSubmitImage,
|
||||
this.focusNode,
|
||||
this.controller,
|
||||
this.decoration,
|
||||
this.onChanged,
|
||||
this.autofocus,
|
||||
this.textInputAction,
|
||||
this.readOnly = false,
|
||||
const ChatInputRowWrapper({
|
||||
required this.controller,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InputBarWrapper> createState() => InputBarWrapperState();
|
||||
State<ChatInputRowWrapper> createState() => ChatInputRowWrapperState();
|
||||
}
|
||||
|
||||
class InputBarWrapperState extends State<InputBarWrapper> {
|
||||
class ChatInputRowWrapperState extends State<ChatInputRowWrapper> {
|
||||
StreamSubscription? _choreoSub;
|
||||
String _currentText = '';
|
||||
|
||||
|
|
@ -50,7 +25,7 @@ class InputBarWrapperState extends State<InputBarWrapper> {
|
|||
void initState() {
|
||||
// Rebuild the widget each time there's an update from choreo
|
||||
_choreoSub =
|
||||
widget.controller?.choreographer.stateListener.stream.listen((_) {
|
||||
widget.controller.choreographer.stateListener.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
|
|
@ -63,10 +38,6 @@ class InputBarWrapperState extends State<InputBarWrapper> {
|
|||
}
|
||||
|
||||
void refreshOnChange(String text) {
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(text);
|
||||
}
|
||||
|
||||
final bool decreasedFromMaxLength =
|
||||
_currentText.length >= PangeaTextController.maxLength &&
|
||||
text.length < PangeaTextController.maxLength;
|
||||
|
|
@ -81,21 +52,5 @@ class InputBarWrapperState extends State<InputBarWrapper> {
|
|||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InputBar(
|
||||
room: widget.room,
|
||||
minLines: widget.minLines,
|
||||
maxLines: widget.maxLines,
|
||||
keyboardType: widget.keyboardType,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onSubmitImage: widget.onSubmitImage,
|
||||
focusNode: widget.focusNode,
|
||||
controller: widget.controller,
|
||||
decoration: widget.decoration,
|
||||
onChanged: refreshOnChange,
|
||||
autofocus: widget.autofocus,
|
||||
textInputAction: widget.textInputAction,
|
||||
readOnly: widget.readOnly,
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) => ChatInputRow(widget.controller);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,12 @@ class ToolbarDisplayController {
|
|||
}
|
||||
|
||||
void showToolbar(BuildContext context, {MessageMode? mode}) {
|
||||
// Close keyboard, if open
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
// Don't show toolbar if keyboard open
|
||||
if (controller.inputFocus.hasFocus) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
return;
|
||||
}
|
||||
|
||||
bool toolbarUp = true;
|
||||
if (highlighted) return;
|
||||
if (controller.selectMode) {
|
||||
|
|
@ -86,13 +90,12 @@ class ToolbarDisplayController {
|
|||
if (targetOffset.dy < 320) {
|
||||
final spaceBeneath = MediaQuery.of(context).size.height -
|
||||
(targetOffset.dy + transformTargetSize.height);
|
||||
// If toolbar is open, opening toolbar beneath without scrolling can cause issues
|
||||
// if (spaceBeneath >= 320) {
|
||||
// toolbarUp = false;
|
||||
// }
|
||||
if (spaceBeneath >= 320) {
|
||||
toolbarUp = false;
|
||||
}
|
||||
|
||||
// See if it's possible to scroll up to make space
|
||||
if (controller.scrollController.offset - targetOffset.dy + 320 >=
|
||||
else if (controller.scrollController.offset - targetOffset.dy + 320 >=
|
||||
controller.scrollController.position.minScrollExtent &&
|
||||
controller.scrollController.offset - targetOffset.dy + 320 <=
|
||||
controller.scrollController.position.maxScrollExtent) {
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/constants/game_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/models/game_state_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Create a timer that counts down to the given time
|
||||
/// Default duration is 180 seconds
|
||||
class RoundTimer extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
const RoundTimer({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
RoundTimerState createState() => RoundTimerState();
|
||||
}
|
||||
|
||||
class RoundTimerState extends State<RoundTimer> {
|
||||
int currentSeconds = 0;
|
||||
Timer? timer;
|
||||
StreamSubscription? stateSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final roundStartTime = widget.controller.currentRound?.currentRoundStart;
|
||||
if (roundStartTime != null) {
|
||||
final roundDuration = DateTime.now().difference(roundStartTime).inSeconds;
|
||||
if (roundDuration > GameConstants.timerMaxSeconds) return;
|
||||
|
||||
currentSeconds = roundDuration;
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||
currentSeconds++;
|
||||
if (currentSeconds >= GameConstants.timerMaxSeconds) {
|
||||
t.cancel();
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
stateSubscription = Matrix.of(context)
|
||||
.client
|
||||
.onRoomState
|
||||
.stream
|
||||
.where(isRoundUpdate)
|
||||
.listen(onRoundUpdate);
|
||||
}
|
||||
|
||||
bool isRoundUpdate(update) {
|
||||
return update.roomId == widget.controller.room.id &&
|
||||
update.state is Event &&
|
||||
(update.state as Event).type == PangeaEventTypes.storyGame;
|
||||
}
|
||||
|
||||
void onRoundUpdate(update) {
|
||||
final GameModel gameState = GameModel.fromJson(
|
||||
(update.state as Event).content,
|
||||
);
|
||||
final startTime = gameState.currentRoundStartTime;
|
||||
final endTime = gameState.previousRoundEndTime;
|
||||
|
||||
if (startTime == null && endTime == null) return;
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
|
||||
// if this update is the start of a round
|
||||
if (startTime != null) {
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||
currentSeconds++;
|
||||
if (currentSeconds >= GameConstants.timerMaxSeconds) {
|
||||
t.cancel();
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// if this update is the end of a round
|
||||
currentSeconds = 0;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
stateSubscription?.cancel();
|
||||
stateSubscription = null;
|
||||
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
|
||||
int get remainingTime => GameConstants.timerMaxSeconds - currentSeconds;
|
||||
|
||||
String get timerText =>
|
||||
'${(remainingTime ~/ 60).toString().padLeft(2, '0')}: ${(remainingTime % 60).toString().padLeft(2, '0')}';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: const Color.fromARGB(255, 126, 22, 14),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(timerText),
|
||||
const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// IconButton(
|
||||
// onPressed: widget.currentRound.timer == null
|
||||
// ? widget.currentRound.startRound
|
||||
// : null,
|
||||
// icon: Icon(
|
||||
// widget.currentRound.timer != null
|
||||
// ? Icons.pause_circle
|
||||
// : Icons.play_circle,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<T?> showAlignedDialog<T>({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
bool barrierDismissible = true,
|
||||
Color? barrierColor = Colors.black54,
|
||||
String? barrierLabel,
|
||||
bool useRootNavigator = true,
|
||||
RouteSettings? routeSettings,
|
||||
Alignment followerAnchor = Alignment.center,
|
||||
Alignment targetAnchor = Alignment.center,
|
||||
Size? refChildSize,
|
||||
Offset offset = Offset.zero,
|
||||
bool avoidOverflow = false,
|
||||
bool isGlobal = false,
|
||||
RouteTransitionsBuilder? transitionsBuilder,
|
||||
Duration? duration,
|
||||
}) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
|
||||
final CapturedThemes themes = InheritedTheme.capture(
|
||||
from: context,
|
||||
to: Navigator.of(
|
||||
context,
|
||||
rootNavigator: useRootNavigator,
|
||||
).context,
|
||||
);
|
||||
|
||||
final RenderBox targetBox = context.findRenderObject()! as RenderBox;
|
||||
final RenderBox overlay =
|
||||
Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
|
||||
Offset position = targetBox
|
||||
.localToGlobal(targetAnchor.alongSize(targetBox.size), ancestor: overlay);
|
||||
|
||||
if (isGlobal) {
|
||||
position = overlay.localToGlobal(followerAnchor.alongSize(overlay.size));
|
||||
}
|
||||
|
||||
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
|
||||
AlignedDialogRoute<T>(
|
||||
followerAlignment: followerAnchor,
|
||||
position: position,
|
||||
context: context,
|
||||
builder: builder,
|
||||
barrierColor: barrierColor,
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierLabel: barrierLabel,
|
||||
useSafeArea: isGlobal == true,
|
||||
settings: routeSettings,
|
||||
themes: themes,
|
||||
transitionsBuilder: transitionsBuilder,
|
||||
duration: duration,
|
||||
avoidOverflow: avoidOverflow,
|
||||
offset: offset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class AlignedDialogRoute<T> extends RawDialogRoute<T> {
|
||||
/// A dialog route with Material entrance and exit animations,
|
||||
/// modal barrier color, and modal barrier behavior (dialog is dismissible
|
||||
/// with a tap on the barrier).
|
||||
AlignedDialogRoute({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
required Alignment followerAlignment,
|
||||
required Offset position,
|
||||
CapturedThemes? themes,
|
||||
super.barrierColor = Colors.transparent,
|
||||
super.barrierDismissible,
|
||||
String? barrierLabel,
|
||||
bool useSafeArea = false,
|
||||
super.settings,
|
||||
RouteTransitionsBuilder? transitionsBuilder,
|
||||
Duration? duration,
|
||||
bool avoidOverflow = false,
|
||||
Offset offset = Offset.zero,
|
||||
}) : super(
|
||||
pageBuilder: (
|
||||
BuildContext buildContext,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
final Widget pageChild = Builder(builder: builder);
|
||||
Widget dialog = Builder(
|
||||
builder: (BuildContext context) {
|
||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||
return CustomSingleChildLayout(
|
||||
delegate: _FollowerDialogRouteLayout(
|
||||
followerAlignment,
|
||||
position,
|
||||
Directionality.of(context),
|
||||
mediaQuery.padding.top,
|
||||
mediaQuery.padding.bottom,
|
||||
offset,
|
||||
avoidOverflow,
|
||||
),
|
||||
child: pageChild,
|
||||
);
|
||||
},
|
||||
);
|
||||
dialog = themes?.wrap(dialog) ?? dialog;
|
||||
if (useSafeArea) {
|
||||
dialog = SafeArea(child: dialog);
|
||||
}
|
||||
return dialog;
|
||||
},
|
||||
barrierLabel: barrierLabel ??
|
||||
MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
transitionDuration: duration ?? const Duration(milliseconds: 200),
|
||||
transitionBuilder:
|
||||
transitionsBuilder ?? _buildMaterialDialogTransitions,
|
||||
);
|
||||
}
|
||||
|
||||
// Positioning of the menu on the screen.
|
||||
class _FollowerDialogRouteLayout extends SingleChildLayoutDelegate {
|
||||
_FollowerDialogRouteLayout(
|
||||
this.followerAnchor,
|
||||
this.position,
|
||||
this.textDirection,
|
||||
this.topPadding,
|
||||
this.bottomPadding,
|
||||
this.offset,
|
||||
this.avoidOverflow,
|
||||
);
|
||||
|
||||
final Alignment followerAnchor;
|
||||
|
||||
final Offset position;
|
||||
|
||||
final TextDirection textDirection;
|
||||
|
||||
final double topPadding;
|
||||
|
||||
final double bottomPadding;
|
||||
|
||||
final Offset offset;
|
||||
final bool avoidOverflow;
|
||||
|
||||
@override
|
||||
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||
// The menu can be at most the size of the overlay minus 8.0 pixels in each
|
||||
// direction.
|
||||
return BoxConstraints.loose(constraints.biggest)
|
||||
.deflate(EdgeInsets.only(top: topPadding, bottom: bottomPadding));
|
||||
}
|
||||
|
||||
@override
|
||||
Offset getPositionForChild(Size size, Size childSize) {
|
||||
Offset rst = followerAnchor.alongSize(childSize);
|
||||
rst = position - rst;
|
||||
rst += offset;
|
||||
if (avoidOverflow) {
|
||||
if (rst.dx < 0) rst = Offset(0, rst.dy);
|
||||
if (rst.dy < 0) rst = Offset(rst.dx, 0);
|
||||
if (rst.dx + childSize.width > size.width) {
|
||||
rst = Offset(size.width - childSize.width, rst.dy);
|
||||
}
|
||||
if (rst.dy + childSize.height > size.height) {
|
||||
rst = Offset(rst.dx, size.height - childSize.height);
|
||||
}
|
||||
}
|
||||
return rst;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_FollowerDialogRouteLayout oldDelegate) {
|
||||
return followerAnchor != oldDelegate.followerAnchor ||
|
||||
position != oldDelegate.position ||
|
||||
offset != oldDelegate.offset ||
|
||||
avoidOverflow != oldDelegate.avoidOverflow ||
|
||||
textDirection != oldDelegate.textDirection ||
|
||||
topPadding != oldDelegate.topPadding ||
|
||||
bottomPadding != oldDelegate.bottomPadding;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMaterialDialogTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return FadeTransition(
|
||||
opacity: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Offset _buildOffSet(
|
||||
BuildContext context, {
|
||||
required Size refChildSize,
|
||||
required Offset offset,
|
||||
}) {
|
||||
final Size screenSize = MediaQuery.of(context).size;
|
||||
final Size maxAvilableArea = Size(
|
||||
screenSize.width - refChildSize.width,
|
||||
screenSize.height - refChildSize.height,
|
||||
);
|
||||
return const Offset(0, 0);
|
||||
}
|
||||
|
|
@ -21,13 +21,18 @@ class ErrorReporter {
|
|||
// Exits early to prevent further execution
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.oopsSomethingWentWrong, // Use the non-null L10n instance to get the error message
|
||||
|
||||
try {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.oopsSomethingWentWrong, // Use the non-null L10n instance to get the error message
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
} catch (err) {
|
||||
debugPrint("Failed to show error snackbar.");
|
||||
}
|
||||
}
|
||||
// final text = '$error\n${stackTrace ?? ''}';
|
||||
// await showAdaptiveDialog(
|
||||
|
|
|
|||
|
|
@ -14,9 +14,25 @@ Future<String?> getDatabaseCipher() async {
|
|||
String? password;
|
||||
|
||||
try {
|
||||
const secureStorage = FlutterSecureStorage();
|
||||
// #Pangea
|
||||
// mogol/flutter_secure_storage#532
|
||||
// mogol/flutter_secure_storage#524
|
||||
// Pangea#
|
||||
const secureStorage = FlutterSecureStorage(
|
||||
// #Pangea
|
||||
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||
// Pangea#
|
||||
);
|
||||
// #Pangea
|
||||
await secureStorage.read(key: _passwordStorageKey);
|
||||
// Pangea#
|
||||
final containsEncryptionKey =
|
||||
await secureStorage.read(key: _passwordStorageKey) != null;
|
||||
// #Pangea
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(message: 'containsEncryptionKey: $containsEncryptionKey'),
|
||||
);
|
||||
// Pangea#
|
||||
if (!containsEncryptionKey) {
|
||||
final rng = Random.secure();
|
||||
final list = Uint8List(32);
|
||||
|
|
@ -29,18 +45,41 @@ Future<String?> getDatabaseCipher() async {
|
|||
}
|
||||
// workaround for if we just wrote to the key and it still doesn't exist
|
||||
password = await secureStorage.read(key: _passwordStorageKey);
|
||||
if (password == null) throw MissingPluginException();
|
||||
if (password == null) {
|
||||
throw MissingPluginException(
|
||||
// #Pangea
|
||||
"password is null after storing new password",
|
||||
// Pangea#
|
||||
);
|
||||
}
|
||||
} on MissingPluginException catch (e) {
|
||||
const FlutterSecureStorage()
|
||||
.delete(key: _passwordStorageKey)
|
||||
.catchError((_) {});
|
||||
Logs().w('Database encryption is not supported on this platform', e);
|
||||
// #Pangea
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message:
|
||||
'Database encryption is not supported on this platform. Error message: ${e.message}',
|
||||
data: {'exception': e},
|
||||
),
|
||||
);
|
||||
// Pangea#
|
||||
_sendNoEncryptionWarning(e);
|
||||
} catch (e, s) {
|
||||
const FlutterSecureStorage()
|
||||
.delete(key: _passwordStorageKey)
|
||||
.catchError((_) {});
|
||||
Logs().w('Unable to init database encryption', e, s);
|
||||
// #Pangea
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: 'Unable to init database encryption',
|
||||
data: {'exception': e, 'stackTrace': s},
|
||||
),
|
||||
);
|
||||
// Pangea#
|
||||
_sendNoEncryptionWarning(e);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -184,9 +184,9 @@ class MatrixLocals extends MatrixLocalizations {
|
|||
@override
|
||||
String redactedAnEvent(Event redactedEvent) {
|
||||
return l10n.redactedAnEvent(
|
||||
// redactedEvent.redactedBecause?.senderFromMemoryOrFallback
|
||||
// .calcDisplayname() ??
|
||||
l10n.user,
|
||||
redactedEvent.redactedBecause?.senderFromMemoryOrFallback
|
||||
.calcDisplayname() ??
|
||||
l10n.user,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -198,8 +198,7 @@ class MatrixLocals extends MatrixLocalizations {
|
|||
@override
|
||||
String removedBy(Event redactedEvent) {
|
||||
return l10n.redactedBy(
|
||||
// redactedEvent.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
"?",
|
||||
redactedEvent.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -347,8 +346,5 @@ class MatrixLocals extends MatrixLocalizations {
|
|||
l10n.startedKeyVerification(senderName);
|
||||
|
||||
@override
|
||||
String invitedBy(String senderName) => l10n.youInvitedBy(senderName);
|
||||
|
||||
@override
|
||||
String get cancelledSend => l10n.canceledSend;
|
||||
String invitedBy(String senderName) => l10n.invitedBy(senderName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
@ -19,21 +20,17 @@ extension RoomStatusExtension on Room {
|
|||
} else if (typingUsers.length == 1) {
|
||||
typingText = L10n.of(context)!.isTyping;
|
||||
if (typingUsers.first.id != directChatMatrixID) {
|
||||
typingText = L10n.of(context)!.userIsTyping(
|
||||
// typingUsers.first.calcDisplayname(),
|
||||
"?",
|
||||
);
|
||||
typingText =
|
||||
L10n.of(context)!.userIsTyping(typingUsers.first.calcDisplayname());
|
||||
}
|
||||
} else if (typingUsers.length == 2) {
|
||||
typingText = L10n.of(context)!.userAndUserAreTyping(
|
||||
// typingUsers.first.calcDisplayname(),
|
||||
// typingUsers[1].calcDisplayname(),
|
||||
"?", "?",
|
||||
typingUsers.first.calcDisplayname(),
|
||||
typingUsers[1].calcDisplayname(),
|
||||
);
|
||||
} else if (typingUsers.length > 2) {
|
||||
typingText = L10n.of(context)!.userAndOthersAreTyping(
|
||||
// typingUsers.first.calcDisplayname(),
|
||||
"?",
|
||||
typingUsers.first.calcDisplayname(),
|
||||
(typingUsers.length - 1).toString(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue