Merge branch 'main' into 2740-if-on-mobile-redirect-from-browser-to-app-or-store
This commit is contained in:
commit
14350c2bfb
49 changed files with 851 additions and 1588 deletions
5
.github/workflows/main_deploy.yaml
vendored
5
.github/workflows/main_deploy.yaml
vendored
|
|
@ -50,6 +50,11 @@ jobs:
|
|||
cp public/.env public/assets/.env
|
||||
touch public/assets/envs.json
|
||||
echo "$ENV_OVERRIDES" >> public/assets/envs.json
|
||||
mkdir -p public/.well-known
|
||||
curl https://app.pangea.chat/.well-known/apple-app-site-association \
|
||||
-o public/.well-known/apple-app-site-association
|
||||
curl https://app.pangea.chat/.well-known/assetlinks.json \
|
||||
-o public/.well-known/assetlinks.json
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ abstract class AppConfig {
|
|||
toolbarButtonsHeight +
|
||||
(chatInputRowOverlayPadding * 2) +
|
||||
toolbarSpacing;
|
||||
static const double audioTranscriptionMaxHeight = 150.0;
|
||||
|
||||
static TextStyle messageTextStyle(
|
||||
Event? event,
|
||||
|
|
|
|||
|
|
@ -5017,9 +5017,6 @@
|
|||
"newDirectMessage": "New direct message",
|
||||
"speakingExercisesTooltip": "Speaking practice",
|
||||
"noChatsFoundHereYet": "No chats found here yet",
|
||||
"endNow": "End now",
|
||||
"setDuration": "Set duration",
|
||||
"activityEnded": "That’s a wrap for this activity! Big thanks to everyone for chatting, learning, and making this space so lively. Language grows with conversation, and every word exchanged brings us closer to confidence and fluency.\n\nKeep practicing, stay curious, and don’t be shy to keep the conversation going!",
|
||||
"duration": "Duration",
|
||||
"transcriptionFailed": "Failed to transcribe audio",
|
||||
"aUserIsKnocking": "1 user is requesting to join your space",
|
||||
|
|
|
|||
|
|
@ -1856,7 +1856,10 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
}
|
||||
|
||||
void pinEvent() {
|
||||
// #Pangea
|
||||
// void pinEvent() {
|
||||
Future<void> pinEvent() async {
|
||||
// Pangea#
|
||||
final pinnedEventIds = room.pinnedEventIds;
|
||||
final selectedEventIds = selectedEvents.map((e) => e.eventId).toSet();
|
||||
final unpin = selectedEventIds.length == 1 &&
|
||||
|
|
@ -1866,10 +1869,16 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
} else {
|
||||
pinnedEventIds.addAll(selectedEventIds);
|
||||
}
|
||||
showFutureLoadingDialog(
|
||||
// #Pangea
|
||||
// showFutureLoadingDialog(
|
||||
// context: context,
|
||||
// future: () => room.setPinnedEvents(pinnedEventIds),
|
||||
// );
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setPinnedEvents(pinnedEventIds),
|
||||
);
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
Timer? _storeInputTimeoutTimer;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
|
|
@ -11,9 +10,7 @@ import 'package:fluffychat/pages/chat/events/message.dart';
|
|||
import 'package:fluffychat/pages/chat/seen_by_row.dart';
|
||||
import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_message.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
|
@ -43,30 +40,6 @@ class ChatEventList extends StatelessWidget {
|
|||
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
|
||||
|
||||
final events = timeline.events.filterByVisibleInGui();
|
||||
// #Pangea
|
||||
if (timeline.room.activityPlan?.endAt != null &&
|
||||
timeline.room.activityPlan!.endAt!.isBefore(DateTime.now())) {
|
||||
final eventIndex = events.indexWhere(
|
||||
(e) => e.originServerTs.isBefore(
|
||||
timeline.room.activityPlan!.endAt!,
|
||||
),
|
||||
);
|
||||
|
||||
if (eventIndex != -1) {
|
||||
events.insert(
|
||||
eventIndex,
|
||||
Event(
|
||||
type: PangeaEventTypes.activityPlanEnd,
|
||||
eventId: timeline.room.client.generateUniqueTransactionId(),
|
||||
senderId: timeline.room.client.userID!,
|
||||
originServerTs: timeline.room.activityPlan!.endAt!,
|
||||
room: timeline.room,
|
||||
content: {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
final animateInEventIndex = controller.animateInEventIndex;
|
||||
|
||||
// create a map of eventId --> index to greatly improve performance of
|
||||
|
|
|
|||
|
|
@ -13,11 +13,9 @@ 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_event_list.dart';
|
||||
import 'package:fluffychat/pages/chat/pinned_events.dart';
|
||||
import 'package:fluffychat/pangea/activities/pinned_activity_message.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
|
@ -190,13 +188,6 @@ class ChatView extends StatelessWidget {
|
|||
if (scrollUpBannerEventId != null) {
|
||||
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
|
||||
}
|
||||
// #Pangea
|
||||
if (controller.room.activityPlan != null &&
|
||||
controller.room.activityPlan!.endAt != null &&
|
||||
controller.room.activityPlan!.endAt!.isAfter(DateTime.now())) {
|
||||
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
|
||||
}
|
||||
// Pangea#
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actionsIconTheme: IconThemeData(
|
||||
|
|
@ -235,9 +226,6 @@ class ChatView extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PinnedEvents(controller),
|
||||
// #Pangea
|
||||
PinnedActivityMessage(controller),
|
||||
// Pangea#
|
||||
if (scrollUpBannerEventId != null)
|
||||
ChatAppBarListTile(
|
||||
leading: IconButton(
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
// #Pangea
|
||||
StreamSubscription? _onAudioPositionChanged;
|
||||
StreamSubscription? _onAudioStateChanged;
|
||||
|
||||
double playbackSpeed = 1.0;
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
|
|
@ -175,6 +177,9 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
: matrix.audioPlayer;
|
||||
|
||||
if (currentPlayer != null) {
|
||||
// #Pangea
|
||||
currentPlayer.setSpeed(playbackSpeed);
|
||||
// Pangea#
|
||||
if (currentPlayer.isAtEndPosition) {
|
||||
currentPlayer.seek(Duration.zero);
|
||||
} else if (currentPlayer.playing) {
|
||||
|
|
@ -204,28 +209,37 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
|
||||
// #Pangea
|
||||
// if (!kIsWeb) {
|
||||
if (!kIsWeb && matrixFile != null) {
|
||||
// Pangea#
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final fileName = Uri.encodeComponent(
|
||||
// #Pangea
|
||||
// widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
|
||||
widget.event!.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
|
||||
if (!kIsWeb) {
|
||||
if (matrixFile != null) {
|
||||
// Pangea#
|
||||
);
|
||||
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final fileName = Uri.encodeComponent(
|
||||
// #Pangea
|
||||
// widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
|
||||
widget.event!.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
|
||||
// Pangea#
|
||||
);
|
||||
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
|
||||
|
||||
await file.writeAsBytes(matrixFile.bytes);
|
||||
await file.writeAsBytes(matrixFile.bytes);
|
||||
|
||||
if (Platform.isIOS &&
|
||||
matrixFile.mimeType.toLowerCase() == 'audio/ogg') {
|
||||
Logs().v('Convert ogg audio file for iOS...');
|
||||
final convertedFile = File('${file.path}.caf');
|
||||
if (await convertedFile.exists() == false) {
|
||||
OpusCaf().convertOpusToCaf(file.path, convertedFile.path);
|
||||
if (Platform.isIOS &&
|
||||
matrixFile.mimeType.toLowerCase() == 'audio/ogg') {
|
||||
Logs().v('Convert ogg audio file for iOS...');
|
||||
final convertedFile = File('${file.path}.caf');
|
||||
if (await convertedFile.exists() == false) {
|
||||
OpusCaf().convertOpusToCaf(file.path, convertedFile.path);
|
||||
}
|
||||
file = convertedFile;
|
||||
}
|
||||
file = convertedFile;
|
||||
// #Pangea
|
||||
} else if (widget.matrixFile != null) {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
|
||||
file = File('${tempDir.path}/${widget.matrixFile!.name}');
|
||||
await file.writeAsBytes(widget.matrixFile!.bytes);
|
||||
}
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
setState(() {
|
||||
|
|
@ -250,6 +264,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
final audioPlayer = matrix.audioPlayer = AudioPlayer();
|
||||
|
||||
// #Pangea
|
||||
audioPlayer.setSpeed(playbackSpeed);
|
||||
_onAudioPositionChanged?.cancel();
|
||||
_onAudioPositionChanged =
|
||||
matrix.audioPlayer!.positionStream.listen((state) {
|
||||
|
|
@ -306,7 +321,22 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
|
||||
void _toggleSpeed() async {
|
||||
final audioPlayer = matrix.audioPlayer;
|
||||
// #Pangea
|
||||
// if (audioPlayer == null) return;
|
||||
switch (playbackSpeed) {
|
||||
case 1.0:
|
||||
setState(() => playbackSpeed = 0.75);
|
||||
case 0.75:
|
||||
setState(() => playbackSpeed = 0.5);
|
||||
case 0.5:
|
||||
setState(() => playbackSpeed = 1.25);
|
||||
case 1.25:
|
||||
setState(() => playbackSpeed = 1.5);
|
||||
default:
|
||||
setState(() => playbackSpeed = 1.0);
|
||||
}
|
||||
if (audioPlayer == null) return;
|
||||
// Pangea#
|
||||
switch (audioPlayer.speed) {
|
||||
// #Pangea
|
||||
// case 1.0:
|
||||
|
|
@ -599,7 +629,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
height: 20,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${audioPlayer?.speed.toString() ?? 1}x',
|
||||
'${audioPlayer?.speed.toString() ?? playbackSpeed}x',
|
||||
style: TextStyle(
|
||||
color: widget.color,
|
||||
fontSize: 9,
|
||||
|
|
|
|||
|
|
@ -155,7 +155,9 @@ class HtmlMessage extends StatelessWidget {
|
|||
|
||||
// #Pangea
|
||||
List<PangeaToken>? get tokens =>
|
||||
pangeaMessageEvent?.messageDisplayRepresentation?.tokens;
|
||||
pangeaMessageEvent?.messageDisplayRepresentation?.tokens
|
||||
?.where((t) => t.pos != "PUNCT")
|
||||
.toList();
|
||||
|
||||
PangeaToken? getToken(
|
||||
String text,
|
||||
|
|
@ -309,6 +311,8 @@ class HtmlMessage extends StatelessWidget {
|
|||
overlayController: overlayController,
|
||||
isTransitionAnimation: isTransitionAnimation,
|
||||
);
|
||||
|
||||
final fontSize = renderer.fontSize(context) ?? this.fontSize;
|
||||
// Pangea#
|
||||
|
||||
switch (node.localName) {
|
||||
|
|
@ -423,10 +427,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
avatar: user.avatarUrl,
|
||||
uri: href,
|
||||
outerContext: context,
|
||||
// #Pangea
|
||||
// fontSize: fontSize,
|
||||
fontSize: renderer.fontSize(context) ?? fontSize,
|
||||
// Pangea#
|
||||
fontSize: fontSize,
|
||||
color: linkStyle.color,
|
||||
// #Pangea
|
||||
userId: user.id,
|
||||
|
|
@ -447,10 +448,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
avatar: room?.avatar,
|
||||
uri: href,
|
||||
outerContext: context,
|
||||
// #Pangea
|
||||
// fontSize: fontSize,
|
||||
fontSize: renderer.fontSize(context) ?? fontSize,
|
||||
// Pangea#
|
||||
fontSize: fontSize,
|
||||
color: linkStyle.color,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_state_event.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.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/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/file_description.dart';
|
||||
|
|
@ -123,22 +121,6 @@ class Message extends StatelessWidget {
|
|||
if (event.type == EventTypes.RoomCreate) {
|
||||
return RoomCreationStateEvent(event: event);
|
||||
}
|
||||
// #Pangea
|
||||
if (event.type == PangeaEventTypes.activityPlan) {
|
||||
final state = event.room.getState(PangeaEventTypes.activityPlan);
|
||||
if (state == null || state is! Event) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return state.originServerTs == event.originServerTs
|
||||
? ActivityStateEvent(event: event)
|
||||
: const SizedBox();
|
||||
}
|
||||
|
||||
if (event.type == PangeaEventTypes.activityPlanEnd) {
|
||||
return const ActivityFinishedEvent();
|
||||
}
|
||||
// Pangea#
|
||||
return StateMessage(event);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -587,7 +587,6 @@ class ChatListController extends State<ChatList>
|
|||
if (space != null) {
|
||||
chatListHandleSpaceTap(
|
||||
context,
|
||||
this,
|
||||
space,
|
||||
);
|
||||
}
|
||||
|
|
@ -669,6 +668,10 @@ class ChatListController extends State<ChatList>
|
|||
|
||||
_activeSpaceId =
|
||||
widget.activeSpaceId == 'clear' ? null : widget.activeSpaceId;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_joinInvitedSpaces();
|
||||
});
|
||||
// Pangea#
|
||||
|
||||
super.initState();
|
||||
|
|
@ -685,6 +688,16 @@ class ChatListController extends State<ChatList>
|
|||
: setActiveSpace(widget.activeSpaceId!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinInvitedSpaces() async {
|
||||
final invitedSpaces = Matrix.of(context).client.rooms.where(
|
||||
(r) => r.isSpace && r.membership == Membership.invite,
|
||||
);
|
||||
|
||||
for (final space in invitedSpaces) {
|
||||
await showInviteDialog(space, context);
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -35,35 +35,6 @@ class InvitationSelectionController extends State<InvitationSelection> {
|
|||
String? get roomId => widget.roomId;
|
||||
|
||||
// #Pangea
|
||||
final viewportKey = GlobalKey();
|
||||
|
||||
final participantListItemHeight = 72.0;
|
||||
final goToChatButtonHeight = 50.0;
|
||||
final shareButtonsHeight = 150.0;
|
||||
final padding = 16.0 * 2;
|
||||
final fixedParticipantHeight = 72.0;
|
||||
|
||||
double? viewportHeight;
|
||||
double get availableHeight =>
|
||||
(viewportHeight ?? 0) -
|
||||
goToChatButtonHeight -
|
||||
shareButtonsHeight -
|
||||
padding;
|
||||
|
||||
bool showShareButtons(int numParticipants) =>
|
||||
(fixedParticipantHeight * numParticipants) < availableHeight;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final context = viewportKey.currentContext;
|
||||
if (context == null) return;
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
setState(() => viewportHeight = size.height);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
List<User>? get participants {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId!);
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_display_name.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/room_settings_constants.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/space_invite_buttons.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
||||
|
|
@ -107,165 +105,153 @@ class InvitationSelectionView extends StatelessWidget {
|
|||
// #Pangea
|
||||
withScrolling: false,
|
||||
// Pangea#
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
width: 450,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendAsset}",
|
||||
errorWidget: (context, url, error) => const SizedBox(),
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
// #Pangea
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 16.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
),
|
||||
// Pangea#
|
||||
child: TextField(
|
||||
textInputAction: TextInputAction.search,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
// #Pangea
|
||||
hintText: L10n.of(context).inviteStudentByUserName,
|
||||
// hintText: L10n.of(context).inviteContactToGroup(groupName),
|
||||
// Pangea#
|
||||
prefixIcon: controller.loading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
horizontal: 12,
|
||||
),
|
||||
child: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.search_outlined),
|
||||
),
|
||||
onChanged: controller.searchUserWithCoolDown,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
// #Pangea
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 16.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
),
|
||||
// Pangea#
|
||||
child: TextField(
|
||||
textInputAction: TextInputAction.search,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
// #Pangea
|
||||
hintText: L10n.of(context).inviteStudentByUserName,
|
||||
// hintText: L10n.of(context).inviteContactToGroup(groupName),
|
||||
// Pangea#
|
||||
prefixIcon: controller.loading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
horizontal: 12,
|
||||
),
|
||||
child: SizedBox.square(
|
||||
dimension: 24,
|
||||
// #Pangea
|
||||
// StreamBuilder<Object>(
|
||||
Expanded(
|
||||
child: StreamBuilder<Object>(
|
||||
// stream: room.client.onRoomState.stream
|
||||
// .where((update) => update.roomId == room.id),
|
||||
stream: room.client.onRoomState.stream
|
||||
.where((update) => update.roomId == room.id)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
// Pangea#
|
||||
builder: (context, snapshot) {
|
||||
final participants =
|
||||
room.getParticipants().map((user) => user.id).toSet();
|
||||
return controller.foundProfiles.isNotEmpty
|
||||
? ListView.builder(
|
||||
// #Pangea
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// shrinkWrap: true,
|
||||
// Pangea#
|
||||
itemCount: controller.foundProfiles.length,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
_InviteContactListTile(
|
||||
profile: controller.foundProfiles[i],
|
||||
isMember: participants.contains(
|
||||
controller.foundProfiles[i].userId,
|
||||
),
|
||||
onTap: () => controller.inviteAction(
|
||||
context,
|
||||
controller.foundProfiles[i].userId,
|
||||
controller.foundProfiles[i].displayName ??
|
||||
controller
|
||||
.foundProfiles[i].userId.localpart ??
|
||||
L10n.of(context).user,
|
||||
),
|
||||
),
|
||||
)
|
||||
: FutureBuilder<List<User>>(
|
||||
future: controller.getContacts(context),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.search_outlined),
|
||||
),
|
||||
onChanged: controller.searchUserWithCoolDown,
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
// StreamBuilder<Object>(
|
||||
Expanded(
|
||||
key: controller.viewportKey,
|
||||
child: StreamBuilder<Object>(
|
||||
// stream: room.client.onRoomState.stream
|
||||
// .where((update) => update.roomId == room.id),
|
||||
stream: room.client.onRoomState.stream
|
||||
.where((update) => update.roomId == room.id)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
// Pangea#
|
||||
builder: (context, snapshot) {
|
||||
final participants =
|
||||
room.getParticipants().map((user) => user.id).toSet();
|
||||
return controller.foundProfiles.isNotEmpty
|
||||
? ListView.builder(
|
||||
);
|
||||
}
|
||||
final contacts = snapshot.data!;
|
||||
return ListView.builder(
|
||||
// #Pangea
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// shrinkWrap: true,
|
||||
// Pangea#
|
||||
itemCount: controller.foundProfiles.length,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
_InviteContactListTile(
|
||||
profile: controller.foundProfiles[i],
|
||||
isMember: participants.contains(
|
||||
controller.foundProfiles[i].userId,
|
||||
),
|
||||
onTap: () => controller.inviteAction(
|
||||
context,
|
||||
controller.foundProfiles[i].userId,
|
||||
controller.foundProfiles[i].displayName ??
|
||||
controller
|
||||
.foundProfiles[i].userId.localpart ??
|
||||
L10n.of(context).user,
|
||||
),
|
||||
),
|
||||
)
|
||||
: FutureBuilder<List<User>>(
|
||||
future: controller.getContacts(context),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
// itemCount: contacts.length,
|
||||
// itemBuilder: (BuildContext context, int i) =>
|
||||
// _InviteContactListTile(
|
||||
itemCount: contacts.length + 1,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i == contacts.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
width: 450,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendAsset}",
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
placeholder: (context, url) =>
|
||||
const Center(
|
||||
child: CircularProgressIndicator
|
||||
.adaptive(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final contacts = snapshot.data!;
|
||||
return ListView.builder(
|
||||
// #Pangea
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// shrinkWrap: true,
|
||||
// itemCount: contacts.length,
|
||||
// itemBuilder: (BuildContext context, int i) =>
|
||||
// _InviteContactListTile(
|
||||
itemCount: contacts.length + 1,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i == contacts.length) {
|
||||
final showButtons = controller
|
||||
.showShareButtons(contacts.length);
|
||||
return AnimatedOpacity(
|
||||
duration:
|
||||
FluffyThemes.animationDuration,
|
||||
opacity: showButtons ? 1.0 : 0.0,
|
||||
child: SpaceInviteButtons(room: room),
|
||||
);
|
||||
}
|
||||
|
||||
return _InviteContactListTile(
|
||||
// Pangea#
|
||||
user: contacts[i],
|
||||
profile: Profile(
|
||||
avatarUrl: contacts[i].avatarUrl,
|
||||
displayName: contacts[i].displayName ??
|
||||
contacts[i].id.localpart ??
|
||||
L10n.of(context).user,
|
||||
userId: contacts[i].id,
|
||||
),
|
||||
isMember:
|
||||
participants.contains(contacts[i].id),
|
||||
onTap: () => controller.inviteAction(
|
||||
context,
|
||||
contacts[i].id,
|
||||
contacts[i].displayName ??
|
||||
contacts[i].id.localpart ??
|
||||
L10n.of(context).user,
|
||||
),
|
||||
);
|
||||
},
|
||||
return _InviteContactListTile(
|
||||
// Pangea#
|
||||
user: contacts[i],
|
||||
profile: Profile(
|
||||
avatarUrl: contacts[i].avatarUrl,
|
||||
displayName: contacts[i].displayName ??
|
||||
contacts[i].id.localpart ??
|
||||
L10n.of(context).user,
|
||||
userId: contacts[i].id,
|
||||
),
|
||||
isMember:
|
||||
participants.contains(contacts[i].id),
|
||||
onTap: () => controller.inviteAction(
|
||||
context,
|
||||
contacts[i].id,
|
||||
contacts[i].displayName ??
|
||||
contacts[i].id.localpart ??
|
||||
L10n.of(context).user,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
|
|
@ -355,6 +341,8 @@ class _InviteContactListTile extends StatelessWidget {
|
|||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
LevelDisplayName(userId: profile.userId),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -87,7 +87,14 @@ class SettingsView extends StatelessWidget {
|
|||
return Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
// #Pangea
|
||||
// padding: const EdgeInsets.all(32.0),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 32.0,
|
||||
bottom: 32.0,
|
||||
left: 12.0,
|
||||
),
|
||||
// Pangea#
|
||||
child: Stack(
|
||||
children: [
|
||||
Avatar(
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ActivityAwareBuilder extends StatefulWidget {
|
||||
final DateTime? deadline;
|
||||
final Widget Function(bool) builder;
|
||||
|
||||
const ActivityAwareBuilder({
|
||||
super.key,
|
||||
required this.builder,
|
||||
this.deadline,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityAwareBuilder> createState() => ActivityAwareBuilderState();
|
||||
}
|
||||
|
||||
class ActivityAwareBuilderState extends State<ActivityAwareBuilder> {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ActivityAwareBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.deadline != widget.deadline) {
|
||||
_setTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setTimer() {
|
||||
final now = DateTime.now();
|
||||
final delay = widget.deadline?.difference(now);
|
||||
|
||||
if (delay != null && delay > Duration.zero) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(delay, () {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(
|
||||
widget.deadline != null && widget.deadline!.isAfter(DateTime.now()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
class ActivityConstants {
|
||||
static const String activityFinishedAsset = "EndActivityMsg.png";
|
||||
}
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
|
||||
class ActivityDurationPopup extends StatefulWidget {
|
||||
final Duration initialValue;
|
||||
const ActivityDurationPopup({
|
||||
super.key,
|
||||
required this.initialValue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityDurationPopup> createState() => ActivityDurationPopupState();
|
||||
}
|
||||
|
||||
class ActivityDurationPopupState extends State<ActivityDurationPopup> {
|
||||
final TextEditingController _daysController = TextEditingController();
|
||||
final TextEditingController _hoursController = TextEditingController();
|
||||
final TextEditingController _minutesController = TextEditingController();
|
||||
|
||||
String? error;
|
||||
|
||||
final List<Duration> _durations = [
|
||||
const Duration(minutes: 15),
|
||||
const Duration(minutes: 30),
|
||||
const Duration(minutes: 45),
|
||||
const Duration(minutes: 60),
|
||||
const Duration(hours: 1, minutes: 30),
|
||||
const Duration(hours: 2),
|
||||
const Duration(hours: 24),
|
||||
const Duration(days: 2),
|
||||
const Duration(days: 7),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_daysController.text = widget.initialValue.inDays.toString();
|
||||
_hoursController.text =
|
||||
widget.initialValue.inHours.remainder(24).toString();
|
||||
_minutesController.text =
|
||||
widget.initialValue.inMinutes.remainder(60).toString();
|
||||
|
||||
_daysController.addListener(() => setState(() => error = null));
|
||||
_hoursController.addListener(() => setState(() => error = null));
|
||||
_minutesController.addListener(() => setState(() => error = null));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_daysController.dispose();
|
||||
_hoursController.dispose();
|
||||
_minutesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setDuration({int? days, int? hours, int? minutes}) {
|
||||
setState(() {
|
||||
if (days != null) _daysController.text = days.toString();
|
||||
if (hours != null) _hoursController.text = hours.toString();
|
||||
if (minutes != null) _minutesController.text = minutes.toString();
|
||||
});
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
final days = duration.inDays;
|
||||
final hours = duration.inHours.remainder(24);
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
|
||||
final List<String> parts = [];
|
||||
if (days > 0) parts.add("${days}d");
|
||||
if (hours > 0) parts.add("${hours}h");
|
||||
if (minutes > 0) parts.add("${minutes}m");
|
||||
if (parts.isEmpty) return "0m";
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
Duration get _duration {
|
||||
final days = int.tryParse(_daysController.text) ?? 0;
|
||||
final hours = int.tryParse(_hoursController.text) ?? 0;
|
||||
final minutes = int.tryParse(_minutesController.text) ?? 0;
|
||||
return Duration(days: days, hours: hours, minutes: minutes);
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
final days = int.tryParse(_daysController.text);
|
||||
final hours = int.tryParse(_hoursController.text);
|
||||
final minutes = int.tryParse(_minutesController.text);
|
||||
|
||||
if (days == null || hours == null || minutes == null) {
|
||||
setState(() {
|
||||
error = "Invalid duration";
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(_duration);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Dialog(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 350.0,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).setDuration,
|
||||
style: const TextStyle(fontSize: 20.0, height: 1.2),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
width: 2,
|
||||
color: theme.colorScheme.primary.withAlpha(100),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12.0,
|
||||
bottom: 12.0,
|
||||
right: 24.0,
|
||||
left: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SelectionArea(
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
_DatePickerInput(
|
||||
type: "d",
|
||||
controller: _daysController,
|
||||
),
|
||||
_DatePickerInput(
|
||||
type: "h",
|
||||
controller: _hoursController,
|
||||
),
|
||||
_DatePickerInput(
|
||||
type: "m",
|
||||
controller: _minutesController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.alarm,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: error != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
error!,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.error,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 24.0,
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 10.0,
|
||||
runSpacing: 10.0,
|
||||
children: _durations
|
||||
.map(
|
||||
(d) => InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
_setDuration(
|
||||
days: d.inDays,
|
||||
hours: d.inHours.remainder(24),
|
||||
minutes: d.inMinutes.remainder(60),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 0.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer
|
||||
.withAlpha(_duration == d ? 200 : 100),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(_formatDuration(d)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _submit,
|
||||
child: Text(L10n.of(context).confirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DatePickerInput extends StatelessWidget {
|
||||
final String type;
|
||||
final TextEditingController controller;
|
||||
|
||||
const _DatePickerInput({
|
||||
required this.type,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 35.0,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
textAlign: TextAlign.end,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.all(0.0),
|
||||
hintText: "0",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 20.0,
|
||||
color: theme.colorScheme.onSurfaceVariant.withAlpha(100),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 20.0,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
Text(type, style: const TextStyle(fontSize: 20.0)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_constants.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_duration_popup.dart';
|
||||
import 'package:fluffychat/pangea/activities/countdown.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
class ActivityStateEvent extends StatefulWidget {
|
||||
final Event event;
|
||||
|
||||
const ActivityStateEvent({required this.event, super.key});
|
||||
|
||||
@override
|
||||
State<ActivityStateEvent> createState() => ActivityStateEventState();
|
||||
}
|
||||
|
||||
class ActivityStateEventState extends State<ActivityStateEvent> {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
final delay = activityPlan?.endAt != null
|
||||
? activityPlan!.endAt!.difference(now)
|
||||
: null;
|
||||
|
||||
if (delay != null && delay > Duration.zero) {
|
||||
_timer = Timer(delay, () {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ActivityPlanModel? get activityPlan {
|
||||
try {
|
||||
return ActivityPlanModel.fromJson(widget.event.content);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (activityPlan == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
final double imageWidth = isColumnMode ? 240.0 : 175.0;
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 400.0,
|
||||
),
|
||||
margin: const EdgeInsets.all(18.0),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: Text(
|
||||
activityPlan!.markdown,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Container(
|
||||
height: imageWidth,
|
||||
width: imageWidth,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: activityPlan!.imageURL != null
|
||||
? activityPlan!.imageURL!.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(
|
||||
activityPlan!.imageURL!,
|
||||
),
|
||||
width: imageWidth,
|
||||
height: imageWidth,
|
||||
cacheKey: activityPlan!.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activityPlan!.imageURL!,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (
|
||||
context,
|
||||
url,
|
||||
error,
|
||||
) =>
|
||||
const SizedBox(),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 9.0,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox.expand(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: () async {
|
||||
final Duration? duration = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivityDurationPopup(
|
||||
initialValue: activityPlan?.duration ??
|
||||
const Duration(days: 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (duration == null) return;
|
||||
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
widget.event.room.sendActivityPlan(
|
||||
activityPlan!.copyWith(
|
||||
endAt: DateTime.now().add(duration),
|
||||
duration: duration,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: CountDown(
|
||||
deadline: activityPlan!.endAt,
|
||||
iconSize: 20.0,
|
||||
textSize: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
), // Optional spacing between buttons
|
||||
Expanded(
|
||||
child: SizedBox.expand(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor: theme.colorScheme.error,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: () {
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
widget.event.room.sendActivityPlan(
|
||||
activityPlan!.copyWith(
|
||||
endAt: DateTime.now(),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
L10n.of(context).endNow,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityFinishedEvent extends StatelessWidget {
|
||||
const ActivityFinishedEvent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 400.0,
|
||||
),
|
||||
margin: const EdgeInsets.all(18.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).activityEnded,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
CachedNetworkImage(
|
||||
width: 120.0,
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${ActivityConstants.activityFinishedAsset}",
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) => const SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
|
||||
class CountDown extends StatefulWidget {
|
||||
final DateTime? deadline;
|
||||
|
||||
final double? iconSize;
|
||||
final double? textSize;
|
||||
|
||||
const CountDown({
|
||||
super.key,
|
||||
required this.deadline,
|
||||
this.iconSize,
|
||||
this.textSize,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CountDown> createState() => CountDownState();
|
||||
}
|
||||
|
||||
class CountDownState extends State<CountDown> {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String? _formatDuration(Duration duration) {
|
||||
final days = duration.inDays;
|
||||
final hours = duration.inHours.remainder(24);
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
|
||||
final List<String> parts = [];
|
||||
if (days > 0) parts.add("${days}d");
|
||||
if (hours > 0) parts.add("${hours}h");
|
||||
if (minutes > 0) parts.add("${minutes}m");
|
||||
if (seconds > 0 && minutes <= 0) parts.add("${seconds}s");
|
||||
if (parts.isEmpty) return null;
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
Duration? get _remainingTime {
|
||||
if (widget.deadline == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
return widget.deadline!.difference(now);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final remainingTime = _remainingTime;
|
||||
final durationString = _formatDuration(remainingTime ?? Duration.zero);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 250.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer_outlined,
|
||||
size: widget.iconSize ?? 28.0,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
remainingTime != null &&
|
||||
remainingTime >= Duration.zero &&
|
||||
durationString != null
|
||||
? durationString
|
||||
: L10n.of(context).duration,
|
||||
style: TextStyle(fontSize: widget.textSize ?? 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_aware_builder.dart';
|
||||
import 'package:fluffychat/pangea/activities/activity_duration_popup.dart';
|
||||
import 'package:fluffychat/pangea/activities/countdown.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class PinnedActivityMessage extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
||||
const PinnedActivityMessage(this.controller, {super.key});
|
||||
|
||||
Future<void> _scrollToEvent() async {
|
||||
final eventId = _activityPlanEvent?.eventId;
|
||||
if (eventId != null) controller.scrollToEventId(eventId);
|
||||
}
|
||||
|
||||
Event? get _activityPlanEvent => controller.timeline?.events.firstWhereOrNull(
|
||||
(event) => event.type == PangeaEventTypes.activityPlan,
|
||||
);
|
||||
|
||||
ActivityPlanModel? get _activityPlan => controller.room.activityPlan;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ActivityAwareBuilder(
|
||||
deadline: _activityPlan?.endAt,
|
||||
builder: (isActive) {
|
||||
if (!isActive || _activityPlan == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ChatAppBarListTile(
|
||||
title: _activityPlan!.title,
|
||||
leading: IconButton(
|
||||
splashRadius: 18,
|
||||
iconSize: 18,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
icon: const Icon(Icons.push_pin),
|
||||
onPressed: () {},
|
||||
),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () async {
|
||||
final Duration? duration = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivityDurationPopup(
|
||||
initialValue:
|
||||
_activityPlan?.duration ?? const Duration(days: 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (duration == null) return;
|
||||
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => controller.room.sendActivityPlan(
|
||||
_activityPlan!.copyWith(
|
||||
endAt: DateTime.now().add(duration),
|
||||
duration: duration,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: CountDown(
|
||||
deadline: _activityPlan!.endAt,
|
||||
iconSize: 16.0,
|
||||
textSize: 14.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: _scrollToEvent,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ class ActivityGeneratorState extends State<ActivityGenerator> {
|
|||
|
||||
ActivitySettingRequestSchema get req => ActivitySettingRequestSchema(
|
||||
langCode:
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode ??
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: _timeout
|
||||
child: (_timeout || !_loading && cards.isEmpty)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RichText(
|
||||
|
|
@ -236,8 +236,10 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
),
|
||||
const TextSpan(text: " "),
|
||||
TextSpan(
|
||||
text:
|
||||
L10n.of(context).activitySuggestionTimeoutMessage,
|
||||
text: _timeout
|
||||
? L10n.of(context)
|
||||
.activitySuggestionTimeoutMessage
|
||||
: L10n.of(context).oopsSomethingWentWrong,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -477,8 +477,8 @@ class GetAnalyticsController extends BaseController {
|
|||
// generate level up analytics as a construct summary
|
||||
ConstructSummary summary;
|
||||
try {
|
||||
final int maxXP = constructListModel.calculateXpWithLevel(upperLevel);
|
||||
final int minXP = constructListModel.calculateXpWithLevel(lowerLevel);
|
||||
final int minXP = constructListModel.calculateXpWithLevel(upperLevel);
|
||||
final int maxXP = constructListModel.calculateXpWithLevel(lowerLevel);
|
||||
int diffXP = maxXP - minXP;
|
||||
if (diffXP < 0) diffXP = 0;
|
||||
|
||||
|
|
@ -492,23 +492,41 @@ class GetAnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
// extract construct use message bodies for analytics
|
||||
List<String?>? constructUseMessageContentBodies = [];
|
||||
final Map<String, Set<String>> useEventIds = {};
|
||||
for (final use in constructUseOfCurrentLevel) {
|
||||
try {
|
||||
final useMessage = await use.getEvent(_client);
|
||||
final useMessageBody = useMessage?.content["body"];
|
||||
if (useMessageBody is String) {
|
||||
constructUseMessageContentBodies.add(useMessageBody);
|
||||
} else {
|
||||
constructUseMessageContentBodies.add(null);
|
||||
}
|
||||
} catch (e) {
|
||||
constructUseMessageContentBodies.add(null);
|
||||
}
|
||||
if (use.metadata.roomId == null) continue;
|
||||
if (use.metadata.eventId == null) continue;
|
||||
useEventIds[use.metadata.roomId!] ??= {};
|
||||
useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!);
|
||||
}
|
||||
if (constructUseMessageContentBodies.length !=
|
||||
constructUseOfCurrentLevel.length) {
|
||||
constructUseMessageContentBodies = null;
|
||||
|
||||
final List<String?> constructUseMessageContentBodies = [];
|
||||
for (final entry in useEventIds.entries) {
|
||||
final String roomId = entry.key;
|
||||
final room = _client.getRoomById(roomId);
|
||||
if (room == null) continue;
|
||||
final List<String?> messageBodies = [];
|
||||
for (final eventId in entry.value) {
|
||||
try {
|
||||
final Event? event = await room.getEventById(eventId);
|
||||
if (event?.content["body"] is! String) continue;
|
||||
final String body = event?.content["body"] as String;
|
||||
if (body.isEmpty) continue;
|
||||
messageBodies.add(body);
|
||||
} catch (e, s) {
|
||||
debugPrint("Error getting event by ID: $e");
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'roomId': roomId,
|
||||
'eventId': eventId,
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
constructUseMessageContentBodies.addAll(messageBodies);
|
||||
}
|
||||
|
||||
final request = ConstructSummaryRequest(
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ class LevelUpBannerState extends State<LevelUpBanner>
|
|||
|
||||
ConstructSummary? _constructSummary;
|
||||
String? _error;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -143,6 +144,7 @@ class LevelUpBannerState extends State<LevelUpBanner>
|
|||
|
||||
Future<void> _setConstructSummary() async {
|
||||
try {
|
||||
setState(() => _loading = true);
|
||||
_constructSummary = await MatrixState.pangeaController.getAnalytics
|
||||
.generateLevelUpAnalytics(
|
||||
widget.level,
|
||||
|
|
@ -150,6 +152,10 @@ class LevelUpBannerState extends State<LevelUpBanner>
|
|||
);
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -364,144 +370,178 @@ class LevelUpBannerState extends State<LevelUpBanner>
|
|||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 24.0,
|
||||
children: [
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: IntrinsicColumnWidth(),
|
||||
1: FlexColumnWidth(),
|
||||
2: IntrinsicColumnWidth(),
|
||||
},
|
||||
defaultVerticalAlignment:
|
||||
TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
...LearningSkillsEnum.values
|
||||
.where(
|
||||
(v) =>
|
||||
v.isVisible && _skillsPoints(v) > -1,
|
||||
child: _loading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
)
|
||||
: _error != null
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Text(
|
||||
L10n.of(context)
|
||||
.oopsSomethingWentWrong,
|
||||
),
|
||||
],
|
||||
)
|
||||
.map((skill) {
|
||||
return TableRow(
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
spacing: 24.0,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 9.0,
|
||||
horizontal: 18.0,
|
||||
),
|
||||
child: Icon(
|
||||
skill.icon,
|
||||
size: 25,
|
||||
color: Colors.white,
|
||||
),
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: IntrinsicColumnWidth(),
|
||||
1: FlexColumnWidth(),
|
||||
2: IntrinsicColumnWidth(),
|
||||
},
|
||||
defaultVerticalAlignment:
|
||||
TableCellVerticalAlignment
|
||||
.middle,
|
||||
children: [
|
||||
...LearningSkillsEnum.values
|
||||
.where(
|
||||
(v) =>
|
||||
v.isVisible &&
|
||||
_skillsPoints(v) > -1,
|
||||
)
|
||||
.map((skill) {
|
||||
return TableRow(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
vertical: 9.0,
|
||||
horizontal: 18.0,
|
||||
),
|
||||
child: Icon(
|
||||
skill.icon,
|
||||
size: 25,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
vertical: 9.0,
|
||||
horizontal: 18.0,
|
||||
),
|
||||
child: Text(
|
||||
skill.tooltip(context),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign:
|
||||
TextAlign.center,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
vertical: 9.0,
|
||||
horizontal: 18.0,
|
||||
),
|
||||
child: Text(
|
||||
"+ ${_skillsPoints(skill)} XP",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign:
|
||||
TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 9.0,
|
||||
horizontal: 18.0,
|
||||
),
|
||||
child: Text(
|
||||
skill.tooltip(context),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
CachedNetworkImage(
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}",
|
||||
width: 400,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
if (_constructSummary?.textSummary !=
|
||||
null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 9.0,
|
||||
horizontal: 18.0,
|
||||
),
|
||||
child: Text(
|
||||
"+ ${_skillsPoints(skill)} XP",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
child: Text(
|
||||
_constructSummary!.textSummary,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
// Share button, currently no functionality
|
||||
// ElevatedButton(
|
||||
// onPressed: () {
|
||||
// // Add share functionality
|
||||
// },
|
||||
// style: ElevatedButton.styleFrom(
|
||||
// backgroundColor: Colors.white,
|
||||
// foregroundColor: Colors.black,
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// vertical: 12,
|
||||
// horizontal: 24,
|
||||
// ),
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// ),
|
||||
// ),
|
||||
// child: const Row(
|
||||
// mainAxisSize: MainAxisSize
|
||||
// .min,
|
||||
// children: [
|
||||
// Text(
|
||||
// "Share with Friends",
|
||||
// style: TextStyle(
|
||||
// fontSize: 16,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 8,
|
||||
// ),
|
||||
// Icon(
|
||||
// Icons.ios_share,
|
||||
// size: 20,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
CachedNetworkImage(
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}",
|
||||
width: 400,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
if (_constructSummary?.textSummary != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_constructSummary!.textSummary,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
// Share button, currently no functionality
|
||||
// ElevatedButton(
|
||||
// onPressed: () {
|
||||
// // Add share functionality
|
||||
// },
|
||||
// style: ElevatedButton.styleFrom(
|
||||
// backgroundColor: Colors.white,
|
||||
// foregroundColor: Colors.black,
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// vertical: 12,
|
||||
// horizontal: 24,
|
||||
// ),
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// ),
|
||||
// ),
|
||||
// child: const Row(
|
||||
// mainAxisSize: MainAxisSize
|
||||
// .min,
|
||||
// children: [
|
||||
// Text(
|
||||
// "Share with Friends",
|
||||
// style: TextStyle(
|
||||
// fontSize: 16,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 8,
|
||||
// ),
|
||||
// Icon(
|
||||
// Icons.ios_share,
|
||||
// size: 20,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -3,16 +3,15 @@ import 'package:flutter/material.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../common/utils/error_handler.dart';
|
||||
|
||||
Future<void> _showInviteDialog(Room room, BuildContext context) async {
|
||||
Future<void> showInviteDialog(Room room, BuildContext context) async {
|
||||
if (room.membership != Membership.invite) return;
|
||||
final acceptInvite = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).youreInvited,
|
||||
|
|
@ -23,7 +22,7 @@ Future<void> _showInviteDialog(Room room, BuildContext context) async {
|
|||
cancelLabel: L10n.of(context).decline,
|
||||
);
|
||||
|
||||
await showFutureLoadingDialog(
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
if (acceptInvite == OkCancelResult.ok) {
|
||||
|
|
@ -31,28 +30,25 @@ Future<void> _showInviteDialog(Room room, BuildContext context) async {
|
|||
context.go(
|
||||
room.isSpace ? "/rooms?spaceId=${room.id}" : "/rooms/${room.id}",
|
||||
);
|
||||
return;
|
||||
return room.id;
|
||||
} else if (acceptInvite == OkCancelResult.cancel) {
|
||||
await room.leave();
|
||||
}
|
||||
await room.leave();
|
||||
},
|
||||
);
|
||||
|
||||
if (!resp.isError && resp.result is String) {
|
||||
context.go("/rooms?spaceId=${resp.result}");
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: curly_braces_in_flow_control_structures
|
||||
void chatListHandleSpaceTap(
|
||||
BuildContext context,
|
||||
ChatListController controller,
|
||||
Room space,
|
||||
) {
|
||||
void setActiveSpaceAndCloseChat() {
|
||||
controller.setActiveSpace(space.id);
|
||||
|
||||
if (FluffyThemes.isColumnMode(context)) {
|
||||
context.go('/rooms/${space.id}');
|
||||
} else if (controller.activeChat != null &&
|
||||
!space.isFirstOrSecondChild(controller.activeChat!)) {
|
||||
context.go("/rooms");
|
||||
}
|
||||
context.go("/rooms?spaceId=${space.id}");
|
||||
}
|
||||
|
||||
void autoJoin(Room space) {
|
||||
|
|
@ -85,7 +81,7 @@ void chatListHandleSpaceTap(
|
|||
justInputtedCode == space.classCode) {
|
||||
// do nothing
|
||||
} else {
|
||||
_showInviteDialog(space, context);
|
||||
showInviteDialog(space, context);
|
||||
}
|
||||
break;
|
||||
case Membership.leave:
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ class DeleteSpaceDialogState extends State<DeleteSpaceDialog> {
|
|||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
||||
|
||||
class SpaceInviteButtons extends StatefulWidget {
|
||||
final Room room;
|
||||
// final ScrollController scrollController;
|
||||
const SpaceInviteButtons({
|
||||
super.key,
|
||||
required this.room,
|
||||
// required this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
SpaceInviteButtonsController createState() => SpaceInviteButtonsController();
|
||||
}
|
||||
|
||||
class SpaceInviteButtonsController extends State<SpaceInviteButtons> {
|
||||
// bool get isVisible {
|
||||
// final context = (widget.key as GlobalKey).currentContext;
|
||||
// if (context == null) return false;
|
||||
|
||||
// final renderBox = context.findRenderObject() as RenderBox;
|
||||
// final position = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
// final size = renderBox.size;
|
||||
// final screenHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
// debugPrint("position: $position, size: $size, screenHeight: $screenHeight");
|
||||
|
||||
// // Check if any part of the widget is within the visible range
|
||||
// return position.dy + size.height > 0 && position.dy < screenHeight;
|
||||
// }
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// WidgetsBinding.instance.addPostFrameCallback(
|
||||
// (_) => debugPrint("isVisible: $isVisible"),
|
||||
// );
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final spaceCode = widget.room.classCode;
|
||||
if (!widget.room.isSpace || spaceCode == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 150.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16.0,
|
||||
right: 16.0,
|
||||
left: 16.0,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.share_outlined,
|
||||
),
|
||||
Text(L10n.of(context).shareSpaceLink),
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
final String initialUrl =
|
||||
kIsWeb ? html.window.origin! : Environment.frontendURL;
|
||||
final link =
|
||||
"$initialUrl/#/join_with_link?${SpaceConstants.classCode}=$spaceCode";
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: link,
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context).copiedToClipboard,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16.0,
|
||||
right: 16.0,
|
||||
left: 16.0,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.share_outlined,
|
||||
),
|
||||
Text(L10n.of(context).shareInviteCode(spaceCode)),
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: spaceCode));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context).copiedToClipboard,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,6 @@ class PangeaEventTypes {
|
|||
static const capacity = "pangea.capacity";
|
||||
|
||||
static const activityPlan = "pangea.activity_plan";
|
||||
static const activityPlanEnd = "pangea.activity.end";
|
||||
|
||||
static const userAge = "pangea.user_age";
|
||||
|
||||
|
|
|
|||
|
|
@ -266,6 +266,21 @@ class PangeaMessageEvent {
|
|||
final botTranscription = SpeechToTextModel.fromJson(
|
||||
Map<String, dynamic>.from(rawBotTranscription),
|
||||
);
|
||||
|
||||
_representations?.add(
|
||||
RepresentationEvent(
|
||||
timeline: timeline,
|
||||
parentMessageEvent: _event,
|
||||
content: PangeaRepresentation(
|
||||
langCode: botTranscription.langCode,
|
||||
text: botTranscription.transcript.text,
|
||||
originalSent: false,
|
||||
originalWritten: false,
|
||||
speechToText: botTranscription,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return botTranscription;
|
||||
}
|
||||
|
||||
|
|
@ -396,7 +411,15 @@ class PangeaMessageEvent {
|
|||
),
|
||||
);
|
||||
}
|
||||
_representations!.add(sent);
|
||||
|
||||
// If originalSent has no tokens, there is not way to generate a tokens event
|
||||
// and send it as a related event, since original sent has not eventID to set
|
||||
// as parentEventId. In this case, it's better to generate a new representation
|
||||
// with an eventID and send the related tokens event to that representation.
|
||||
// This is a rare situation, and has only been seen with some bot messages.
|
||||
if (sent.tokens != null) {
|
||||
_representations!.add(sent);
|
||||
}
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
m: "error parsing originalSent",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
|
|
|||
16
lib/pangea/extensions/pangea_rooms_chunk_extension.dart
Normal file
16
lib/pangea/extensions/pangea_rooms_chunk_extension.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:matrix/matrix_api_lite/generated/model.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
||||
|
||||
extension PangeaRoomsChunk on PublicRoomsChunk {
|
||||
/// Use Random with a seed to get the default
|
||||
/// avatar associated with this space
|
||||
String defaultAvatar() {
|
||||
final int seed = roomId.hashCode;
|
||||
return SpaceConstants.publicSpaceIcons[Random(seed).nextInt(
|
||||
SpaceConstants.publicSpaceIcons.length,
|
||||
)];
|
||||
}
|
||||
}
|
||||
|
|
@ -277,6 +277,57 @@ extension EventsRoomExtension on Room {
|
|||
}) async {
|
||||
BookmarkedActivitiesRepo.save(activity);
|
||||
|
||||
String? imageURL = activity.imageURL;
|
||||
final eventId = await pangeaSendTextEvent(
|
||||
activity.markdown,
|
||||
messageTag: ModelKey.messageTagActivityPlan,
|
||||
);
|
||||
|
||||
Uint8List? bytes = avatar;
|
||||
if (imageURL != null && bytes == null) {
|
||||
try {
|
||||
final resp = await http
|
||||
.get(Uri.parse(imageURL))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
bytes = resp.bodyBytes;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"avatarURL": imageURL,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes != null && imageURL == null) {
|
||||
final url = await client.uploadContent(
|
||||
bytes,
|
||||
filename: filename,
|
||||
);
|
||||
imageURL = url.toString();
|
||||
}
|
||||
|
||||
MatrixFile? file;
|
||||
if (filename != null && bytes != null) {
|
||||
file = MatrixFile(
|
||||
bytes: bytes,
|
||||
name: filename,
|
||||
);
|
||||
}
|
||||
|
||||
if (file != null) {
|
||||
final content = <String, dynamic>{
|
||||
'msgtype': file.msgType,
|
||||
'body': file.name,
|
||||
'filename': file.name,
|
||||
'url': imageURL,
|
||||
ModelKey.messageTags: ModelKey.messageTagActivityPlan,
|
||||
};
|
||||
await sendEvent(content);
|
||||
}
|
||||
|
||||
if (canChangeStateEvent(PangeaEventTypes.activityPlan)) {
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
|
|
@ -284,6 +335,10 @@ extension EventsRoomExtension on Room {
|
|||
"",
|
||||
activity.toJson(),
|
||||
);
|
||||
|
||||
if (eventId != null && canChangeStateEvent(EventTypes.RoomPinnedEvents)) {
|
||||
await setPinnedEvents([eventId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicator_button.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_rooms_chunk_extension.dart';
|
||||
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
||||
|
|
@ -35,14 +37,26 @@ class PublicSpaceTile extends StatelessWidget {
|
|||
height: isColumnMode ? 80.0 : 58.0,
|
||||
child: Row(
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: space.avatarUrl,
|
||||
name: space.name,
|
||||
size: isColumnMode ? 80.0 : 58.0,
|
||||
borderRadius: BorderRadius.circular(
|
||||
10,
|
||||
),
|
||||
),
|
||||
(space.avatarUrl != null)
|
||||
? Avatar(
|
||||
mxContent: space.avatarUrl,
|
||||
name: space.name,
|
||||
size: isColumnMode ? 80.0 : 58.0,
|
||||
borderRadius: BorderRadius.circular(
|
||||
10,
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
10,
|
||||
),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: space.defaultAvatar(),
|
||||
width: isColumnMode ? 80.0 : 58.0,
|
||||
height: isColumnMode ? 80.0 : 58.0,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
|
|
|||
|
|
@ -125,10 +125,12 @@ class SettingsLearningController extends State<SettingsLearning> {
|
|||
if (formKey.currentState!.validate()) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async => pangeaController.userController.updateProfile(
|
||||
(_) => _profile,
|
||||
waitForDataInSync: true,
|
||||
),
|
||||
future: () async => pangeaController.userController
|
||||
.updateProfile(
|
||||
(_) => _profile,
|
||||
waitForDataInSync: true,
|
||||
)
|
||||
.timeout(const Duration(seconds: 15)),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,17 +80,16 @@ class OnboardingController extends State<Onboarding> {
|
|||
Future<void> startChatWithBot() async {
|
||||
final resp = await showFutureLoadingDialog<String>(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.createRoom(
|
||||
invite: [BotName.byEnvironment],
|
||||
isDirect: true,
|
||||
preset: CreateRoomPreset.trustedPrivateChat,
|
||||
initialState: [
|
||||
BotOptionsModel(mode: BotMode.directChat).toStateEvent,
|
||||
RoomDefaults.defaultPowerLevels(
|
||||
Matrix.of(context).client.userID!,
|
||||
),
|
||||
],
|
||||
future: () => Matrix.of(context).client.startDirectChat(
|
||||
BotName.byEnvironment,
|
||||
preset: CreateRoomPreset.trustedPrivateChat,
|
||||
initialState: [
|
||||
BotOptionsModel(mode: BotMode.directChat).toStateEvent,
|
||||
RoomDefaults.defaultPowerLevels(
|
||||
Matrix.of(context).client.userID!,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (resp.isError) return;
|
||||
context.go("/rooms/${resp.result}");
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class PhoneticTranscriptionWidget extends StatefulWidget {
|
|||
|
||||
final bool enabled;
|
||||
|
||||
final VoidCallback? onTranscriptionFetched;
|
||||
|
||||
const PhoneticTranscriptionWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
|
|
@ -30,6 +32,7 @@ class PhoneticTranscriptionWidget extends StatefulWidget {
|
|||
this.iconSize,
|
||||
this.iconColor,
|
||||
this.enabled = true,
|
||||
this.onTranscriptionFetched,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -51,6 +54,17 @@ class _PhoneticTranscriptionWidgetState
|
|||
_fetchTranscription();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(
|
||||
covariant PhoneticTranscriptionWidget oldWidget,
|
||||
) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.text != widget.text ||
|
||||
oldWidget.textLanguage != widget.textLanguage) {
|
||||
_fetchTranscription();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchTranscription() async {
|
||||
try {
|
||||
setState(() {
|
||||
|
|
@ -92,7 +106,12 @@ class _PhoneticTranscriptionWidgetState
|
|||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
widget.onTranscriptionFetched?.call();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
|
@ -9,7 +7,7 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_rooms_chunk_extension.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
|
@ -212,10 +210,7 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
|
|||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: SpaceConstants
|
||||
.publicSpaceIcons[Random().nextInt(
|
||||
SpaceConstants.publicSpaceIcons.length,
|
||||
)],
|
||||
imageUrl: chunk!.defaultAvatar(),
|
||||
width: 160.0,
|
||||
height: 160.0,
|
||||
fit: BoxFit.cover,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
|
@ -7,8 +5,8 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_rooms_chunk_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/widgets/mxc_image.dart';
|
||||
|
||||
class PublicSpaceCard extends StatelessWidget {
|
||||
|
|
@ -71,10 +69,7 @@ class PublicSpaceCard extends StatelessWidget {
|
|||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: SpaceConstants
|
||||
.publicSpaceIcons[Random().nextInt(
|
||||
SpaceConstants.publicSpaceIcons.length,
|
||||
)],
|
||||
imageUrl: space.defaultAvatar(),
|
||||
width: width,
|
||||
height: width,
|
||||
fit: BoxFit.cover,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class SubscriptionController extends BaseController {
|
|||
final bool hasSubscription =
|
||||
currentSubscriptionInfo?.currentSubscriptionId != null;
|
||||
|
||||
return hasSubscription;
|
||||
return hasSubscription || _userController.inTrialWindow();
|
||||
}
|
||||
|
||||
bool _isInitializing = false;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
|
|
@ -96,9 +95,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onPrimary,
|
||||
)
|
||||
: const CardErrorWidget(
|
||||
error: "Null audio file in message_audio_card",
|
||||
);
|
||||
: const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar
|
|||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_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/lemmas/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
|
|
@ -101,6 +99,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
bool showSpeechTranslation = false;
|
||||
String? speechTranslation;
|
||||
|
||||
final StreamController contentChangedStream = StreamController.broadcast();
|
||||
|
||||
double maxWidth = AppConfig.toolbarMinWidth;
|
||||
|
||||
/////////////////////////////////////
|
||||
|
|
@ -121,6 +121,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => widget.chatController.clearSelectedEvents(),
|
||||
);
|
||||
contentChangedStream.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +133,11 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
RepresentationEvent? repEvent =
|
||||
pangeaMessageEvent?.messageDisplayRepresentation;
|
||||
repEvent ??= await _fetchNewRepEvent();
|
||||
|
||||
if (repEvent == null ||
|
||||
(repEvent.event == null && repEvent.tokens == null)) {
|
||||
repEvent = await _fetchNewRepEvent();
|
||||
}
|
||||
|
||||
if (repEvent?.event != null) {
|
||||
await repEvent!.sendTokensEvent(
|
||||
|
|
@ -142,24 +147,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
MatrixState.pangeaController.languageController.userL2!.langCode,
|
||||
);
|
||||
}
|
||||
// If repEvent is originalSent but it's missing tokens, then fetch tokens.
|
||||
// An edge case, but has happened with some bot message.
|
||||
else if (repEvent != null &&
|
||||
repEvent.tokens == null &&
|
||||
repEvent.content.originalSent) {
|
||||
final tokens = await repEvent.tokensGlobal(
|
||||
pangeaMessageEvent!.senderId,
|
||||
pangeaMessageEvent!.event.originServerTs,
|
||||
);
|
||||
await pangeaMessageEvent!.room.pangeaSendTextEvent(
|
||||
pangeaMessageEvent!.messageDisplayText,
|
||||
editEventId: pangeaMessageEvent!.eventId,
|
||||
originalSent: pangeaMessageEvent!.originalSent?.content,
|
||||
originalWritten: pangeaMessageEvent!.originalWritten?.content,
|
||||
tokensSent: PangeaMessageTokens(tokens: tokens),
|
||||
choreo: pangeaMessageEvent!.originalSent?.choreo,
|
||||
);
|
||||
}
|
||||
|
||||
// Get all the lemma infos
|
||||
final messageVocabConstructIds = pangeaMessageEvent!
|
||||
|
|
@ -587,7 +574,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
void setTranslation(String value) {
|
||||
if (mounted) {
|
||||
setState(() => translation = value);
|
||||
setState(() {
|
||||
translation = value;
|
||||
contentChangedStream.add(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -598,12 +588,18 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
}
|
||||
|
||||
if (showTranslation == show) return;
|
||||
setState(() => showTranslation = show);
|
||||
setState(() {
|
||||
showTranslation = show;
|
||||
contentChangedStream.add(true);
|
||||
});
|
||||
}
|
||||
|
||||
void setSpeechTranslation(String value) {
|
||||
if (mounted) {
|
||||
setState(() => speechTranslation = value);
|
||||
setState(() {
|
||||
speechTranslation = value;
|
||||
contentChangedStream.add(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -614,7 +610,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
}
|
||||
|
||||
if (showSpeechTranslation == show) return;
|
||||
setState(() => showSpeechTranslation = show);
|
||||
setState(() {
|
||||
showSpeechTranslation = show;
|
||||
contentChangedStream.add(true);
|
||||
});
|
||||
}
|
||||
|
||||
void setTranscription(SpeechToTextModel value) {
|
||||
|
|
@ -622,13 +621,17 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
setState(() {
|
||||
transcriptionError = null;
|
||||
transcription = value;
|
||||
contentChangedStream.add(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void setTranscriptionError(String value) {
|
||||
if (mounted) {
|
||||
setState(() => transcriptionError = value);
|
||||
setState(() {
|
||||
transcriptionError = value;
|
||||
contentChangedStream.add(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
Offset? _currentOffset;
|
||||
|
||||
StreamSubscription? _reactionSubscription;
|
||||
StreamSubscription? _contentChangedSubscription;
|
||||
|
||||
final _animationDuration = const Duration(
|
||||
milliseconds: AppConfig.overlayAnimationDuration,
|
||||
|
|
@ -106,6 +107,10 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
},
|
||||
).listen((_) => setState(() {}));
|
||||
|
||||
_contentChangedSubscription = widget
|
||||
.overlayController.contentChangedStream.stream
|
||||
.listen(_onContentSizeChanged);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await _centeredMessageCompleter.future;
|
||||
if (!mounted) return;
|
||||
|
|
@ -138,6 +143,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
void dispose() {
|
||||
_animationController.dispose();
|
||||
_reactionSubscription?.cancel();
|
||||
_contentChangedSubscription?.cancel();
|
||||
MatrixState.pangeaController.matrixState.audioPlayer
|
||||
?..stop()
|
||||
..dispose();
|
||||
|
|
@ -196,34 +202,9 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
}
|
||||
|
||||
if (mode == ReadingAssistanceMode.selectMode) {
|
||||
_overlayOffsetAnimation = Tween<Offset>(
|
||||
begin: _currentOffset,
|
||||
end: _adjustedOriginalMessageOffset,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
),
|
||||
)..addListener(() {
|
||||
if (mounted) {
|
||||
setState(() => _currentOffset = _overlayOffsetAnimation?.value);
|
||||
}
|
||||
});
|
||||
_resetOffsetAnimation(_adjustedOriginalMessageOffset);
|
||||
} else if (mode == ReadingAssistanceMode.practiceMode) {
|
||||
_overlayOffsetAnimation = Tween<Offset>(
|
||||
begin: _currentOffset,
|
||||
end: _centeredMessageOffset!,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
),
|
||||
)..addListener(() {
|
||||
if (mounted) {
|
||||
setState(() => _currentOffset = _overlayOffsetAnimation?.value);
|
||||
}
|
||||
});
|
||||
|
||||
_resetOffsetAnimation(_centeredMessageOffset!);
|
||||
_messageSizeAnimation = Tween<Size>(
|
||||
begin: Size(
|
||||
_originalMessageSize.width,
|
||||
|
|
@ -244,6 +225,40 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
}
|
||||
}
|
||||
|
||||
void _onContentSizeChanged(_) {
|
||||
Future.delayed(FluffyThemes.animationDuration, () {
|
||||
final offset = _overlayMessageRenderBox?.localToGlobal(Offset.zero);
|
||||
if (offset == null || !_overlayMessageRenderBox!.hasSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final newOffset = _adjustedMessageOffset(
|
||||
_overlayMessageRenderBox!.size,
|
||||
offset,
|
||||
);
|
||||
|
||||
if (newOffset == _currentOffset) return;
|
||||
_resetOffsetAnimation(newOffset);
|
||||
_animationController.forward(from: 0);
|
||||
});
|
||||
}
|
||||
|
||||
void _resetOffsetAnimation(Offset offset) {
|
||||
_overlayOffsetAnimation = Tween<Offset>(
|
||||
begin: _currentOffset,
|
||||
end: offset,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
),
|
||||
)..addListener(() {
|
||||
if (mounted) {
|
||||
setState(() => _currentOffset = _overlayOffsetAnimation?.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
T _runWithLogging<T>(
|
||||
Function runner,
|
||||
String errorMessage,
|
||||
|
|
@ -326,6 +341,14 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
null,
|
||||
);
|
||||
|
||||
RenderBox? get _overlayMessageRenderBox => _runWithLogging<RenderBox?>(
|
||||
() => MatrixState.pAnyState.getRenderBox(
|
||||
'overlay_message_${widget.event.eventId}',
|
||||
),
|
||||
"Error getting overlay message render box",
|
||||
null,
|
||||
);
|
||||
|
||||
Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100);
|
||||
|
||||
/// The size of the message in the chat list (as opposed to the expanded size in the center overlay)
|
||||
|
|
@ -394,17 +417,28 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
}
|
||||
|
||||
Offset get _adjustedOriginalMessageOffset {
|
||||
return _adjustedMessageOffset(
|
||||
_originalMessageSize,
|
||||
_originalMessageOffset,
|
||||
);
|
||||
}
|
||||
|
||||
Offset _adjustedMessageOffset(
|
||||
Size messageSize,
|
||||
Offset messageOffset,
|
||||
) {
|
||||
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
|
||||
return _defaultMessageOffset;
|
||||
}
|
||||
|
||||
final topOffset = _originalMessageOffset.dy;
|
||||
final bottomOffset = _originalMessageBottomOffset -
|
||||
_reactionsHeight -
|
||||
_selectionButtonsHeight;
|
||||
final topOffset = messageOffset.dy;
|
||||
final bottomOffset =
|
||||
(_mediaQuery!.size.height - topOffset - messageSize.height) -
|
||||
_reactionsHeight -
|
||||
_selectionButtonsHeight;
|
||||
|
||||
final hasHeaderOverflow = topOffset <
|
||||
(_headerHeight + AppConfig.toolbarSpacing + _audioTranscriptionHeight);
|
||||
final hasHeaderOverflow =
|
||||
topOffset < (_headerHeight + AppConfig.toolbarSpacing);
|
||||
final hasFooterOverflow =
|
||||
bottomOffset < (_footerHeight + AppConfig.toolbarSpacing);
|
||||
|
||||
|
|
@ -416,15 +450,12 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
}
|
||||
|
||||
if (hasHeaderOverflow) {
|
||||
final difference = topOffset -
|
||||
(_headerHeight +
|
||||
AppConfig.toolbarSpacing +
|
||||
_audioTranscriptionHeight);
|
||||
final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing);
|
||||
|
||||
double newBottomOffset = _mediaQuery!.size.height -
|
||||
_originalMessageOffset.dy +
|
||||
topOffset +
|
||||
difference -
|
||||
_originalMessageSize.height -
|
||||
messageSize.height -
|
||||
_selectionButtonsHeight;
|
||||
|
||||
if (newBottomOffset < _footerHeight + AppConfig.toolbarSpacing) {
|
||||
|
|
@ -524,12 +555,6 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
return showSelectionButtons ? AppConfig.toolbarButtonsHeight : 0;
|
||||
}
|
||||
|
||||
double get _audioTranscriptionHeight {
|
||||
return widget.pangeaMessageEvent?.isAudioMessage ?? false
|
||||
? AppConfig.audioTranscriptionMaxHeight
|
||||
: 0;
|
||||
}
|
||||
|
||||
bool get _hasReactions {
|
||||
final reactionsEvents = widget.event.aggregatedEvents(
|
||||
widget.chatController.timeline!,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dar
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class OverlayCenterContent extends StatelessWidget {
|
||||
final Event event;
|
||||
|
|
@ -69,6 +70,11 @@ class OverlayCenterContent extends StatelessWidget {
|
|||
MeasureRenderBox(
|
||||
onChange: onChangeMessageSize,
|
||||
child: OverlayMessage(
|
||||
key: isTransitionAnimation
|
||||
? MatrixState.pAnyState
|
||||
.layerLinkAndKey('overlay_message_${event.eventId}')
|
||||
.key
|
||||
: null,
|
||||
event,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: chatController.choreographer.immersionMode,
|
||||
|
|
|
|||
|
|
@ -109,10 +109,15 @@ class OverlayHeaderState extends State<OverlayHeader> {
|
|||
icon: pinned
|
||||
? const Icon(Icons.push_pin)
|
||||
: const Icon(Icons.push_pin_outlined),
|
||||
onPressed: controller.pinEvent,
|
||||
onPressed: () {
|
||||
controller
|
||||
.pinEvent()
|
||||
.then((_) => setState(() {}));
|
||||
},
|
||||
tooltip: pinned ? l10n.unpin : l10n.pinMessage,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
|
||||
if (controller.canEditSelectedEvents &&
|
||||
!controller.selectedEvents.first.isActivityMessage)
|
||||
IconButton(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_content.dart';
|
||||
|
|
@ -19,6 +22,7 @@ import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart
|
|||
import 'package:fluffychat/pangea/toolbar/widgets/stt_transcript_tokens.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/file_description.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
// @ggurdin be great to explain the need/function of a widget like this
|
||||
|
|
@ -149,9 +153,13 @@ class OverlayMessage extends StatelessWidget {
|
|||
|
||||
final transcription = showTranscription
|
||||
? Container(
|
||||
width: messageWidth,
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: AppConfig.audioTranscriptionMaxHeight,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(
|
||||
FluffyThemes.columnWidth * 1.5,
|
||||
MediaQuery.of(context).size.width -
|
||||
(ownMessage ? 0 : Avatar.defaultSize) -
|
||||
24.0,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
|
|
@ -178,6 +186,7 @@ class OverlayMessage extends StatelessWidget {
|
|||
child: Column(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SttTranscriptTokens(
|
||||
model: overlayController.transcription!,
|
||||
|
|
@ -197,8 +206,8 @@ class OverlayMessage extends StatelessWidget {
|
|||
text: overlayController
|
||||
.transcription!.transcript.text,
|
||||
textLanguage: PLanguageStore.byLangCode(
|
||||
pangeaMessageEvent!
|
||||
.messageDisplayLangCode,
|
||||
overlayController
|
||||
.transcription!.langCode,
|
||||
) ??
|
||||
LanguageModel.unknown,
|
||||
style: AppConfig.messageTextStyle(
|
||||
|
|
@ -208,6 +217,9 @@ class OverlayMessage extends StatelessWidget {
|
|||
iconColor: textColor,
|
||||
enabled:
|
||||
event.senderId != BotName.byEnvironment,
|
||||
onTranscriptionFetched: () =>
|
||||
overlayController.contentChangedStream
|
||||
.add(true),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -226,9 +238,13 @@ class OverlayMessage extends StatelessWidget {
|
|||
|
||||
final translation = showTranslation || showSpeechTranslation
|
||||
? Container(
|
||||
width: messageWidth,
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: AppConfig.audioTranscriptionMaxHeight,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(
|
||||
FluffyThemes.columnWidth * 1.5,
|
||||
MediaQuery.of(context).size.width -
|
||||
(ownMessage ? 0 : Avatar.defaultSize) -
|
||||
24.0,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
|
|
@ -271,8 +287,6 @@ class OverlayMessage extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (readingAssistanceMode == ReadingAssistanceMode.transitionMode)
|
||||
transcription,
|
||||
if (event.relationshipType == RelationshipTypes.reply)
|
||||
FutureBuilder<Event?>(
|
||||
future: event.getReplyEvent(
|
||||
|
|
@ -371,8 +385,6 @@ class OverlayMessage extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (readingAssistanceMode == ReadingAssistanceMode.transitionMode)
|
||||
translation,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -386,26 +398,31 @@ class OverlayMessage extends StatelessWidget {
|
|||
color: noBubble ? Colors.transparent : color,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (readingAssistanceMode != ReadingAssistanceMode.transitionMode)
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 1.5,
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
transcription,
|
||||
sizeAnimation != null
|
||||
? AnimatedBuilder(
|
||||
animation: sizeAnimation!,
|
||||
builder: (context, child) {
|
||||
return SizedBox(
|
||||
height: sizeAnimation!.value.height,
|
||||
width: sizeAnimation!.value.width,
|
||||
child: content,
|
||||
);
|
||||
},
|
||||
)
|
||||
: content,
|
||||
if (readingAssistanceMode != ReadingAssistanceMode.transitionMode)
|
||||
sizeAnimation != null
|
||||
? AnimatedBuilder(
|
||||
animation: sizeAnimation!,
|
||||
builder: (context, child) {
|
||||
return SizedBox(
|
||||
height: sizeAnimation!.value.height,
|
||||
width: sizeAnimation!.value.width,
|
||||
child: content,
|
||||
);
|
||||
},
|
||||
)
|
||||
: content,
|
||||
translation,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -126,7 +125,8 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
void _clear() {
|
||||
setState(() {
|
||||
_audioError = null;
|
||||
// Audio errors do not go away when I switch modes and back
|
||||
// Is there any reason to wipe error records on clear?
|
||||
_translationError = null;
|
||||
_speechTranslationError = null;
|
||||
});
|
||||
|
|
@ -149,8 +149,10 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
}
|
||||
|
||||
setState(
|
||||
() => _selectedMode =
|
||||
_selectedMode == mode && mode != SelectMode.audio ? null : mode,
|
||||
() => _selectedMode = _selectedMode == mode &&
|
||||
(mode != SelectMode.audio || _audioError != null)
|
||||
? null
|
||||
: mode,
|
||||
);
|
||||
|
||||
if (_selectedMode == SelectMode.audio) {
|
||||
|
|
@ -202,12 +204,10 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
File? file;
|
||||
file = File('${tempDir.path}/${_audioBytes!.name}');
|
||||
await file.writeAsBytes(_audioBytes!.bytes);
|
||||
setState(() => _audioFile = file);
|
||||
_audioFile = file;
|
||||
}
|
||||
|
||||
if (mounted) setState(() => _isLoadingAudio = false);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
_audioError = e.toString();
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
|
|
@ -217,6 +217,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
messageEvent?.messageDisplayLangCode,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoadingAudio = false);
|
||||
}
|
||||
}
|
||||
|
|
@ -240,7 +241,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
matrix?.audioPlayer?.dispose();
|
||||
matrix?.audioPlayer = AudioPlayer();
|
||||
matrix?.voiceMessageEventId.value =
|
||||
widget.overlayController.pangeaMessageEvent?.eventId;
|
||||
"${widget.overlayController.pangeaMessageEvent?.eventId}_button";
|
||||
|
||||
_onPlayerStateChanged =
|
||||
matrix?.audioPlayer?.playerStateStream.listen((state) {
|
||||
|
|
@ -289,7 +290,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
}
|
||||
|
||||
TtsController.stop();
|
||||
matrix?.audioPlayer?.play();
|
||||
await matrix?.audioPlayer?.play();
|
||||
} catch (e, s) {
|
||||
setState(() => _audioError = e.toString());
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -487,25 +488,28 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
spacing: 4.0,
|
||||
children: [
|
||||
for (final mode in modes)
|
||||
Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: PressableButton(
|
||||
depressed: mode == _selectedMode,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
onPressed: () => _updateMode(mode),
|
||||
playSound: true,
|
||||
colorFactor: Theme.of(context).brightness == Brightness.light
|
||||
? 0.55
|
||||
: 0.3,
|
||||
child: Container(
|
||||
height: buttonSize,
|
||||
width: buttonSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
TooltipVisibility(
|
||||
visible: (!_isError || mode != _selectedMode),
|
||||
child: Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: PressableButton(
|
||||
depressed: mode == _selectedMode,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
onPressed: () => _updateMode(mode),
|
||||
playSound: mode != SelectMode.audio,
|
||||
colorFactor: Theme.of(context).brightness == Brightness.light
|
||||
? 0.55
|
||||
: 0.3,
|
||||
child: Container(
|
||||
height: buttonSize,
|
||||
width: buttonSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: icon(mode),
|
||||
),
|
||||
child: icon(mode),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -73,15 +73,18 @@ class WordZoomWidget extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
token.text.content,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? AppConfig.yellowDark
|
||||
: AppConfig.yellowLight,
|
||||
Flexible(
|
||||
child: Text(
|
||||
token.text.content,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? AppConfig.yellowDark
|
||||
: AppConfig.yellowLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
ConstructXpWidget(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,36 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import '../../config/app_config.dart';
|
||||
|
||||
extension VisibleInGuiExtension on List<Event> {
|
||||
List<Event> filterByVisibleInGui({String? exceptionEventId}) => where(
|
||||
(event) => event.isVisibleInGui || event.eventId == exceptionEventId,
|
||||
).toList();
|
||||
List<Event> filterByVisibleInGui({String? exceptionEventId}) {
|
||||
final visibleEvents =
|
||||
where((e) => e.isVisibleInGui || e.eventId == exceptionEventId)
|
||||
.toList();
|
||||
|
||||
// Hide creation state events:
|
||||
if (visibleEvents.isNotEmpty &&
|
||||
visibleEvents.last.type == EventTypes.RoomCreate) {
|
||||
var i = visibleEvents.length - 2;
|
||||
while (i > 0) {
|
||||
final event = visibleEvents[i];
|
||||
if (!event.isState) break;
|
||||
if (event.type == EventTypes.Encryption) {
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (event.type == EventTypes.RoomMember &&
|
||||
event.roomMemberChangeType == RoomMemberChangeType.acceptInvite) {
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
visibleEvents.removeAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
return visibleEvents;
|
||||
}
|
||||
}
|
||||
|
||||
extension IsStateExtension on Event {
|
||||
|
|
@ -23,12 +46,7 @@ extension IsStateExtension on Event {
|
|||
// if we enabled to hide all redacted events, don't show those
|
||||
(!AppConfig.hideRedactedEvents || !redacted) &&
|
||||
// if we enabled to hide all unknown events, don't show those
|
||||
// #Pangea
|
||||
// (!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
|
||||
(!AppConfig.hideUnknownEvents ||
|
||||
isEventTypeKnown ||
|
||||
importantStateEvents.contains(type)) &&
|
||||
// Pangea#
|
||||
(!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
|
||||
// remove state events that we don't want to render
|
||||
(isState || !AppConfig.hideAllStateEvents) &&
|
||||
// #Pangea
|
||||
|
|
@ -64,8 +82,6 @@ extension IsStateExtension on Event {
|
|||
EventTypes.RoomMember,
|
||||
EventTypes.RoomTombstone,
|
||||
EventTypes.CallInvite,
|
||||
PangeaEventTypes.activityPlan,
|
||||
PangeaEventTypes.activityPlanEnd,
|
||||
};
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:fluffychat/config/app_config.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_list/navi_rail_item.dart';
|
||||
import 'package:fluffychat/pangea/chat_list/utils/chat_list_handle_space_tap.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
|
@ -175,7 +176,20 @@ class SpacesNavigationRail extends StatelessWidget {
|
|||
return NaviRailItem(
|
||||
toolTip: displayname,
|
||||
isSelected: activeSpaceId == space.id,
|
||||
onTap: () => onGoToSpaceId(rootSpaces[i].id),
|
||||
// #Pangea
|
||||
// onTap: () => onGoToSpaceId(rootSpaces[i].id),
|
||||
onTap: () {
|
||||
final room = client.getRoomById(rootSpaces[i].id);
|
||||
if (room != null) {
|
||||
chatListHandleSpaceTap(
|
||||
context,
|
||||
room,
|
||||
);
|
||||
} else {
|
||||
onGoToSpaceId(rootSpaces[i].id);
|
||||
}
|
||||
},
|
||||
// Pangea#
|
||||
unreadBadgeFilter: (room) =>
|
||||
spaceChildrenIds.contains(room.id),
|
||||
icon: Avatar(
|
||||
|
|
|
|||
|
|
@ -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+2
|
||||
version: 4.1.12+1
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue