From f16bfc53672869e12e9bc5c473c414d8e8f91274 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:33:40 -0400 Subject: [PATCH 01/14] chore: make audio marker color more consistent --- lib/pages/chat/events/audio_player.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 10e230d4b..6719ab0d1 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -540,8 +540,8 @@ class AudioPlayerState extends State { thumbColor: widget.senderId == Matrix.of(context).client.userID // Pangea# - ? theme.colorScheme.onPrimary - : theme.colorScheme.primary, + ? widget.color + : theme.colorScheme.onSurface, activeColor: waveform == null ? widget.color : Colors.transparent, From 3b0dfdc2ca6767465ab35891cfbde4cd1230d4f1 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:44:23 -0400 Subject: [PATCH 02/14] fixed pangea# tag --- lib/pages/chat/events/audio_player.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 6719ab0d1..2131dd868 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -537,11 +537,13 @@ class AudioPlayerState extends State { // #Pangea // thumbColor: widget.event.senderId == // widget.event.room.client.userID + // ? theme.colorScheme.onPrimary + // : theme.colorScheme.primary, thumbColor: widget.senderId == Matrix.of(context).client.userID - // Pangea# ? widget.color : theme.colorScheme.onSurface, + // Pangea# activeColor: waveform == null ? widget.color : Colors.transparent, From c84676074e123c812099078f6f3e702f87401182 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 24 Jun 2025 11:13:45 -0400 Subject: [PATCH 03/14] chore: unfocus keyboard on click level up notification --- lib/pangea/analytics_misc/level_up.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart index 3eea090e0..ab57cd03d 100644 --- a/lib/pangea/analytics_misc/level_up.dart +++ b/lib/pangea/analytics_misc/level_up.dart @@ -176,6 +176,8 @@ class LevelUpBannerState extends State Future _toggleDetails() async { if (!Environment.isStagingEnvironment) return; + FocusScope.of(context).unfocus(); + if (mounted) { setState(() { _showDetails = !_showDetails; From 584e4e1f84eb7af9f8ff52a6f5d193fbcca80564 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 24 Jun 2025 11:33:26 -0400 Subject: [PATCH 04/14] chore: replace mic icon with 1x default playback speed icon --- lib/pages/chat/events/audio_player.dart | 83 ++++++++++++++++--------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 2131dd868..11bbfba52 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -585,43 +585,68 @@ class AudioPlayerState extends State { ), // Pangea# const SizedBox(width: 8), - AnimatedCrossFade( - firstChild: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Icon( - Icons.mic_none_outlined, - color: widget.color, - ), - ), - secondChild: Material( - color: widget.color.withAlpha(64), + // #Pangea + Material( + color: widget.color.withAlpha(64), + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + child: InkWell( borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: InkWell( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - onTap: _toggleSpeed, - child: SizedBox( - width: 32, - height: 20, - child: Center( - child: Text( - '${audioPlayer?.speed.toString()}x', - style: TextStyle( - color: widget.color, - fontSize: 9, - ), + onTap: _toggleSpeed, + child: SizedBox( + width: 32, + height: 20, + child: Center( + child: Text( + '${audioPlayer?.speed.toString() ?? 1}x', + style: TextStyle( + color: widget.color, + fontSize: 9, ), ), ), ), ), - alignment: Alignment.center, - crossFadeState: audioPlayer == null - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: FluffyThemes.animationDuration, ), + // AnimatedCrossFade( + // firstChild: Padding( + // padding: const EdgeInsets.only(right: 8.0), + // child: Icon( + // Icons.mic_none_outlined, + // color: widget.color, + // ), + // ), + // secondChild: Material( + // color: widget.color.withAlpha(64), + // borderRadius: + // BorderRadius.circular(AppConfig.borderRadius), + // child: InkWell( + // borderRadius: + // BorderRadius.circular(AppConfig.borderRadius), + // onTap: _toggleSpeed, + // child: SizedBox( + // width: 32, + // height: 20, + // child: Center( + // child: Text( + // '${audioPlayer?.speed.toString()}x', + // style: TextStyle( + // color: widget.color, + // fontSize: 9, + // ), + // ), + // ), + // ), + // ), + // ), + // alignment: Alignment.center, + // crossFadeState: audioPlayer == null + // ? CrossFadeState.showFirst + // : CrossFadeState.showSecond, + // duration: FluffyThemes.animationDuration, + // ), + // Pangea# ], ), ), From 2f21216db5cb5b904fb6e12368351ca33246e5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Wed, 18 Jun 2025 08:48:31 +0200 Subject: [PATCH 05/14] fix: Ban button displayed for already banned users --- lib/widgets/member_actions_popup_menu_button.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/member_actions_popup_menu_button.dart b/lib/widgets/member_actions_popup_menu_button.dart index f4ff7c3ca..6f30e930f 100644 --- a/lib/widgets/member_actions_popup_menu_button.dart +++ b/lib/widgets/member_actions_popup_menu_button.dart @@ -170,7 +170,7 @@ void showMemberActionsPopupMenu({ ], ), ), - if (user.canBan) + if (user.canBan && user.membership != Membership.ban) PopupMenuItem( value: _MemberActions.ban, child: Row( From 596217d2d60ada24015628b771b3e623e60a61fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sat, 21 Jun 2025 10:05:14 +0200 Subject: [PATCH 06/14] fix: Workaround for reversed width and height of compressed videos sent from Android --- lib/pages/chat/events/video_player.dart | 13 +++++++++---- lib/pages/chat/send_file_dialog.dart | 18 ++++++++++++------ lib/utils/resize_video.dart | 15 ++++++++++----- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index 856bda50c..6b31eb774 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; @@ -36,11 +38,14 @@ class EventVideoPlayer extends StatelessWidget { .tryGet('xyz.amorgan.blurhash') ?? fallbackBlurHash; final fileDescription = event.fileDescription; + const maxDimension = 300.0; final infoMap = event.content.tryGetMap('info'); - final videoWidth = infoMap?.tryGet('w') ?? 400; - final videoHeight = infoMap?.tryGet('h') ?? 300; - const height = 300.0; - final width = videoWidth * (height / videoHeight); + final videoWidth = infoMap?.tryGet('w') ?? maxDimension; + final videoHeight = infoMap?.tryGet('h') ?? maxDimension; + + final modifier = max(videoWidth, videoHeight) / maxDimension; + final width = videoWidth / modifier; + final height = videoHeight / modifier; final durationInt = infoMap?.tryGet('duration'); final duration = diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index c786b6769..7b215496e 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -59,16 +59,22 @@ class SendFileDialogState extends State { final length = await xfile.length(); final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path); + // Generate video thumbnail + if (PlatformInfos.isMobile && + mimeType != null && + mimeType.startsWith('video')) { + scaffoldMessenger.showLoadingSnackBar(l10n.generatingVideoThumbnail); + thumbnail = await xfile.getVideoThumbnail(); + } + // If file is a video, shrink it! if (PlatformInfos.isMobile && mimeType != null && - mimeType.startsWith('video') && - length > minSizeToCompress && - compress) { + mimeType.startsWith('video')) { scaffoldMessenger.showLoadingSnackBar(l10n.compressVideo); - file = await xfile.resizeVideo(); - scaffoldMessenger.showLoadingSnackBar(l10n.generatingVideoThumbnail); - thumbnail = await xfile.getVideoThumbnail(); + file = await xfile.getVideoInfo( + compress: length > minSizeToCompress && compress, + ); } else { if (length > maxUploadSize) { throw FileTooBigMatrixException(length, maxUploadSize); diff --git a/lib/utils/resize_video.dart b/lib/utils/resize_video.dart index 768677106..99a4c6b0a 100644 --- a/lib/utils/resize_video.dart +++ b/lib/utils/resize_video.dart @@ -8,22 +8,27 @@ extension ResizeImage on XFile { static const int max = 1200; static const int quality = 40; - Future resizeVideo() async { + Future getVideoInfo({bool compress = true}) async { MediaInfo? mediaInfo; try { if (PlatformInfos.isMobile) { // will throw an error e.g. on Android SDK < 18 - mediaInfo = await VideoCompress.compressVideo(path); + mediaInfo = compress + ? await VideoCompress.compressVideo(path, deleteOrigin: true) + : await VideoCompress.getMediaInfo(path); } } catch (e, s) { - Logs().w('Error while compressing video', e, s); + Logs().w('Error while fetching video media info', e, s); } + return MatrixVideoFile( bytes: (await mediaInfo?.file?.readAsBytes()) ?? await readAsBytes(), name: name, mimeType: mimeType, - width: mediaInfo?.width, - height: mediaInfo?.height, + // on Android width and height is reversed: + // https://github.com/jonataslaw/VideoCompress/issues/172 + width: PlatformInfos.isAndroid ? mediaInfo?.height : mediaInfo?.width, + height: PlatformInfos.isAndroid ? mediaInfo?.width : mediaInfo?.height, duration: mediaInfo?.duration?.round(), ); } From 7bc67174563235729d9cbb7799d51421376a97eb Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 24 Jun 2025 14:20:35 -0400 Subject: [PATCH 07/14] chore: fix overflow in word card text --- .../widgets/word_zoom/word_zoom_widget.dart | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index a3e9481cb..dcb86b6eb 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -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( From 889741c083fb80531ae774c0b8abaae38fb4be86 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Tue, 24 Jun 2025 14:31:56 -0400 Subject: [PATCH 08/14] Make word zoom header flexible, so it doesn't overflow --- .../widgets/word_zoom/word_zoom_widget.dart | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index a3e9481cb..813d7235c 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -73,15 +73,17 @@ 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, + style: TextStyle( + fontSize: 32.0, + fontWeight: FontWeight.w600, + height: 1.2, + color: Theme.of(context).brightness == Brightness.light + ? AppConfig.yellowDark + : AppConfig.yellowLight, + ), ), ), ConstructXpWidget( From d76cb4752da29673f9053109b3b8e0bc84043369 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 24 Jun 2025 14:44:48 -0400 Subject: [PATCH 09/14] chore: allow user to set playback speed before audio starts playing --- lib/pages/chat/events/audio_player.dart | 28 +++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 11bbfba52..6728e69c0 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -77,6 +77,8 @@ class AudioPlayerState extends State { // #Pangea StreamSubscription? _onAudioPositionChanged; StreamSubscription? _onAudioStateChanged; + + double playbackSpeed = 1.0; // Pangea# @override @@ -175,6 +177,9 @@ class AudioPlayerState extends State { : matrix.audioPlayer; if (currentPlayer != null) { + // #Pangea + currentPlayer.setSpeed(playbackSpeed); + // Pangea# if (currentPlayer.isAtEndPosition) { currentPlayer.seek(Duration.zero); } else if (currentPlayer.playing) { @@ -250,6 +255,7 @@ class AudioPlayerState extends State { final audioPlayer = matrix.audioPlayer = AudioPlayer(); // #Pangea + audioPlayer.setSpeed(playbackSpeed); _onAudioPositionChanged?.cancel(); _onAudioPositionChanged = matrix.audioPlayer!.positionStream.listen((state) { @@ -306,7 +312,25 @@ class AudioPlayerState extends State { void _toggleSpeed() async { final audioPlayer = matrix.audioPlayer; - if (audioPlayer == null) return; + // #Pangea + // if (audioPlayer == null) return; + if (audioPlayer == null || + matrix.voiceMessageEventId.value != widget.eventId) { + 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); + } + return; + } + // Pangea# switch (audioPlayer.speed) { // #Pangea // case 1.0: @@ -599,7 +623,7 @@ class AudioPlayerState extends State { height: 20, child: Center( child: Text( - '${audioPlayer?.speed.toString() ?? 1}x', + '${audioPlayer?.speed.toString() ?? playbackSpeed}x', style: TextStyle( color: widget.color, fontSize: 9, From 646723d7cab3fa5c511a789c4e095ce44083ddcc Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 24 Jun 2025 15:04:03 -0400 Subject: [PATCH 10/14] chore: update pin button after pinning / unpinning a message --- lib/pages/chat/chat.dart | 13 +++++++++++-- lib/pangea/toolbar/widgets/overlay_header.dart | 7 ++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index e2cd8b515..3b2a0dedf 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1856,7 +1856,10 @@ class ChatController extends State } } - void pinEvent() { + // #Pangea + // void pinEvent() { + Future 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 } else { pinnedEventIds.addAll(selectedEventIds); } - showFutureLoadingDialog( + // #Pangea + // showFutureLoadingDialog( + // context: context, + // future: () => room.setPinnedEvents(pinnedEventIds), + // ); + await showFutureLoadingDialog( context: context, future: () => room.setPinnedEvents(pinnedEventIds), ); + // Pangea# } Timer? _storeInputTimeoutTimer; diff --git a/lib/pangea/toolbar/widgets/overlay_header.dart b/lib/pangea/toolbar/widgets/overlay_header.dart index a13e1c2a4..81bb4aba4 100644 --- a/lib/pangea/toolbar/widgets/overlay_header.dart +++ b/lib/pangea/toolbar/widgets/overlay_header.dart @@ -109,10 +109,15 @@ class OverlayHeaderState extends State { 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( From a688e1c38b1feecc9e45fbbd4012c938d0686098 Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Tue, 24 Jun 2025 15:23:20 -0400 Subject: [PATCH 11/14] setup well-known dir for staging --- .github/workflows/main_deploy.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index d60208739..51dfaa1fe 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -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: From 4c11a2a079c23d7bfbefee0c9615d72309f33e3e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 24 Jun 2025 16:05:41 -0400 Subject: [PATCH 12/14] chore: update phonetic transcription when text changes --- .../phonetic_transcription_widget.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index 8486a9991..83aaf2275 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -51,6 +51,17 @@ class _PhoneticTranscriptionWidgetState _fetchTranscription(); } + @override + void didUpdateWidget( + covariant PhoneticTranscriptionWidget oldWidget, + ) { + super.didUpdateWidget(oldWidget); + if (oldWidget.text != widget.text || + oldWidget.textLanguage != widget.textLanguage) { + _fetchTranscription(); + } + } + Future _fetchTranscription() async { try { setState(() { From f86608bb0a69a45840da0952e86c43a038f8183d Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:21:35 -0400 Subject: [PATCH 13/14] Revert "chore: add fake event to show when activity ended" --- lib/pages/chat/chat_event_list.dart | 27 -- lib/pages/chat/events/message.dart | 4 - .../activities/activity_state_event.dart | 333 +++++++++--------- .../events/constants/pangea_event_types.dart | 1 - .../filtered_timeline_extension.dart | 31 +- 5 files changed, 190 insertions(+), 206 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 030f81422..948c0578b 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -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 diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 30d787343..0fd7e79bc 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -134,10 +134,6 @@ class Message extends StatelessWidget { ? ActivityStateEvent(event: event) : const SizedBox(); } - - if (event.type == PangeaEventTypes.activityPlanEnd) { - return const ActivityFinishedEvent(); - } // Pangea# return StateMessage(event); } diff --git a/lib/pangea/activities/activity_state_event.dart b/lib/pangea/activities/activity_state_event.dart index b59b8c233..1f7a250e5 100644 --- a/lib/pangea/activities/activity_state_event.dart +++ b/lib/pangea/activities/activity_state_event.dart @@ -57,6 +57,11 @@ class ActivityStateEventState extends State { } } + bool get _activityIsOver { + return activityPlan?.endAt != null && + DateTime.now().isAfter(activityPlan!.endAt!); + } + @override Widget build(BuildContext context) { if (activityPlan == null) { @@ -78,151 +83,186 @@ class ActivityStateEventState extends State { spacing: 12.0, children: [ Container( - padding: const EdgeInsets.all(16.0), + padding: EdgeInsets.all(_activityIsOver ? 24.0 : 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, - ), - ), + child: _activityIsOver + ? 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(), + ), + ], + ) + : 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, + child: _activityIsOver + ? const SizedBox() + : IntrinsicHeight( + child: Row( + spacing: 12.0, 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, - ), - ), + Container( + height: imageWidth, + width: imageWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), ), - ), // Optional spacing between buttons + 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: 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: 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, + ), ), ), - ); - }, - child: Text( - L10n.of(context).endNow, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, ), ), - ), + ], ), ), ], ), ), - ], - ), - ), ), ], ), @@ -230,50 +270,3 @@ class ActivityStateEventState extends State { ); } } - -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(), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pangea/events/constants/pangea_event_types.dart b/lib/pangea/events/constants/pangea_event_types.dart index a385fe97d..fe2a47e18 100644 --- a/lib/pangea/events/constants/pangea_event_types.dart +++ b/lib/pangea/events/constants/pangea_event_types.dart @@ -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"; diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index d762a4dcc..f074a43f1 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -5,9 +5,33 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import '../../config/app_config.dart'; extension VisibleInGuiExtension on List { - List filterByVisibleInGui({String? exceptionEventId}) => where( - (event) => event.isVisibleInGui || event.eventId == exceptionEventId, - ).toList(); + List 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 { @@ -65,7 +89,6 @@ extension IsStateExtension on Event { EventTypes.RoomTombstone, EventTypes.CallInvite, PangeaEventTypes.activityPlan, - PangeaEventTypes.activityPlanEnd, }; // Pangea# } From 57a8c09da254dc7621a6c5a5428035509a48818b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 24 Jun 2025 16:31:20 -0400 Subject: [PATCH 14/14] chore: revert changes to activity plan message --- lib/l10n/intl_en.arb | 3 - lib/pages/chat/chat_view.dart | 12 - lib/pages/chat/events/message.dart | 18 -- .../activities/activity_aware_builder.dart | 62 ---- lib/pangea/activities/activity_constants.dart | 3 - .../activities/activity_duration_popup.dart | 282 ------------------ .../activities/activity_state_event.dart | 279 ----------------- lib/pangea/activities/countdown.dart | 98 ------ .../activities/pinned_activity_message.dart | 100 ------- .../extensions/pangea_room_extension.dart | 1 + .../extensions/room_events_extension.dart | 55 ++++ .../filtered_timeline_extension.dart | 10 +- 12 files changed, 57 insertions(+), 866 deletions(-) delete mode 100644 lib/pangea/activities/activity_aware_builder.dart delete mode 100644 lib/pangea/activities/activity_constants.dart delete mode 100644 lib/pangea/activities/activity_duration_popup.dart delete mode 100644 lib/pangea/activities/activity_state_event.dart delete mode 100644 lib/pangea/activities/countdown.dart delete mode 100644 lib/pangea/activities/pinned_activity_message.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0f6b2899e..c5c74d682 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -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", diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 2f90e7f59..1436cb00b 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -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( diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 30d787343..ada6cceb4 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -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); } diff --git a/lib/pangea/activities/activity_aware_builder.dart b/lib/pangea/activities/activity_aware_builder.dart deleted file mode 100644 index 68eea54af..000000000 --- a/lib/pangea/activities/activity_aware_builder.dart +++ /dev/null @@ -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 createState() => ActivityAwareBuilderState(); -} - -class ActivityAwareBuilderState extends State { - 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()), - ); - } -} diff --git a/lib/pangea/activities/activity_constants.dart b/lib/pangea/activities/activity_constants.dart deleted file mode 100644 index 41858d53a..000000000 --- a/lib/pangea/activities/activity_constants.dart +++ /dev/null @@ -1,3 +0,0 @@ -class ActivityConstants { - static const String activityFinishedAsset = "EndActivityMsg.png"; -} diff --git a/lib/pangea/activities/activity_duration_popup.dart b/lib/pangea/activities/activity_duration_popup.dart deleted file mode 100644 index c6cbb56ab..000000000 --- a/lib/pangea/activities/activity_duration_popup.dart +++ /dev/null @@ -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 createState() => ActivityDurationPopupState(); -} - -class ActivityDurationPopupState extends State { - final TextEditingController _daysController = TextEditingController(); - final TextEditingController _hoursController = TextEditingController(); - final TextEditingController _minutesController = TextEditingController(); - - String? error; - - final List _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 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)), - ], - ); - } -} diff --git a/lib/pangea/activities/activity_state_event.dart b/lib/pangea/activities/activity_state_event.dart deleted file mode 100644 index b59b8c233..000000000 --- a/lib/pangea/activities/activity_state_event.dart +++ /dev/null @@ -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 createState() => ActivityStateEventState(); -} - -class ActivityStateEventState extends State { - 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(), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pangea/activities/countdown.dart b/lib/pangea/activities/countdown.dart deleted file mode 100644 index 17516cb23..000000000 --- a/lib/pangea/activities/countdown.dart +++ /dev/null @@ -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 createState() => CountDownState(); -} - -class CountDownState extends State { - 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 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), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pangea/activities/pinned_activity_message.dart b/lib/pangea/activities/pinned_activity_message.dart deleted file mode 100644 index 8be5a492f..000000000 --- a/lib/pangea/activities/pinned_activity_message.dart +++ /dev/null @@ -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 _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, - ); - }, - ); - } -} diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 58616de36..316ef7199 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -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'; diff --git a/lib/pangea/extensions/room_events_extension.dart b/lib/pangea/extensions/room_events_extension.dart index e24427ef3..799ebdb66 100644 --- a/lib/pangea/extensions/room_events_extension.dart +++ b/lib/pangea/extensions/room_events_extension.dart @@ -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 = { + '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]); + } } } diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index d762a4dcc..a350316b5 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -1,7 +1,6 @@ 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 { @@ -23,12 +22,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 +58,6 @@ extension IsStateExtension on Event { EventTypes.RoomMember, EventTypes.RoomTombstone, EventTypes.CallInvite, - PangeaEventTypes.activityPlan, - PangeaEventTypes.activityPlanEnd, }; // Pangea# }