Merge branch 'main' into 3163-expand-the-box-for-long-message
This commit is contained in:
commit
f06b49e1c4
25 changed files with 247 additions and 958 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:
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -250,6 +255,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 +312,25 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
|
||||
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:
|
||||
|
|
@ -537,11 +561,13 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
// #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#
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.primary,
|
||||
? widget.color
|
||||
: theme.colorScheme.onSurface,
|
||||
// Pangea#
|
||||
activeColor: waveform == null
|
||||
? widget.color
|
||||
: Colors.transparent,
|
||||
|
|
@ -583,43 +609,68 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
),
|
||||
// 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() ?? playbackSpeed}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#
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String>('xyz.amorgan.blurhash') ??
|
||||
fallbackBlurHash;
|
||||
final fileDescription = event.fileDescription;
|
||||
const maxDimension = 300.0;
|
||||
final infoMap = event.content.tryGetMap<String, Object?>('info');
|
||||
final videoWidth = infoMap?.tryGet<int>('w') ?? 400;
|
||||
final videoHeight = infoMap?.tryGet<int>('h') ?? 300;
|
||||
const height = 300.0;
|
||||
final width = videoWidth * (height / videoHeight);
|
||||
final videoWidth = infoMap?.tryGet<int>('w') ?? maxDimension;
|
||||
final videoHeight = infoMap?.tryGet<int>('h') ?? maxDimension;
|
||||
|
||||
final modifier = max(videoWidth, videoHeight) / maxDimension;
|
||||
final width = videoWidth / modifier;
|
||||
final height = videoHeight / modifier;
|
||||
|
||||
final durationInt = infoMap?.tryGet<int>('duration');
|
||||
final duration =
|
||||
|
|
|
|||
|
|
@ -59,16 +59,22 @@ class SendFileDialogState extends State<SendFileDialog> {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -176,6 +176,8 @@ class LevelUpBannerState extends State<LevelUpBanner>
|
|||
Future<void> _toggleDetails() async {
|
||||
if (!Environment.isStagingEnvironment) return;
|
||||
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_showDetails = !_showDetails;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void> _fetchTranscription() async {
|
||||
try {
|
||||
setState(() {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,22 +8,27 @@ extension ResizeImage on XFile {
|
|||
static const int max = 1200;
|
||||
static const int quality = 40;
|
||||
|
||||
Future<MatrixVideoFile> resizeVideo() async {
|
||||
Future<MatrixVideoFile> 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(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ void showMemberActionsPopupMenu({
|
|||
],
|
||||
),
|
||||
),
|
||||
if (user.canBan)
|
||||
if (user.canBan && user.membership != Membership.ban)
|
||||
PopupMenuItem(
|
||||
value: _MemberActions.ban,
|
||||
child: Row(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue