Merge pull request #3074 from pangeachat/3050-activity-duration-in-chat-activity-planner-end-flow

feat: initial work for add duration to in-chat activities
pull/2245/head
ggurdin 5 months ago committed by GitHub
commit c1fad2c05e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5017,6 +5017,10 @@
"newDirectMessage": "New direct message",
"speakingExercisesTooltip": "Speaking practice",
"noChatsFoundHereYet": "No chats found here yet",
"endNow": "End now",
"setDuration": "Set duration",
"activityEnded": "Thats 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 dont be shy to keep the conversation going!",
"duration": "Duration",
"transcriptionFailed": "Failed to transcribe audio",
"aUserIsKnocking": "1 user is requesting to join your space",
"usersAreKnocking": "{users} users are requesting to join your space",

@ -13,9 +13,11 @@ 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';
@ -188,6 +190,11 @@ class ChatView extends StatelessWidget {
if (scrollUpBannerEventId != null) {
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
// #Pangea
if (controller.room.activityPlan != null) {
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
// Pangea#
return Scaffold(
appBar: AppBar(
actionsIconTheme: IconThemeData(
@ -226,6 +233,9 @@ class ChatView extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
PinnedEvents(controller),
// #Pangea
PinnedActivityMessage(controller),
// Pangea#
if (scrollUpBannerEventId != null)
ChatAppBarListTile(
leading: IconButton(

@ -9,7 +9,9 @@ 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';
@ -121,6 +123,18 @@ 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();
}
// Pangea#
return StateMessage(event);
}

@ -0,0 +1,62 @@
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()),
);
}
}

@ -0,0 +1,3 @@
class ActivityConstants {
static const String activityFinishedAsset = "EndActivityMsg.png";
}

@ -0,0 +1,280 @@
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: [
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)),
],
);
}
}

@ -0,0 +1,272 @@
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;
}
}
bool get _activityIsOver {
return activityPlan?.endAt != null &&
DateTime.now().isAfter(activityPlan!.endAt!);
}
@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: EdgeInsets.all(_activityIsOver ? 24.0 : 16.0),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(18),
),
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
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: _activityIsOver
? const SizedBox()
: 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,
),
),
),
),
),
],
),
),
],
),
),
),
],
),
),
);
}
}

@ -0,0 +1,98 @@
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),
),
),
],
),
);
}
}

@ -0,0 +1,100 @@
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,
);
},
);
}
}

@ -11,6 +11,8 @@ class ActivityPlanModel {
final String instructions;
final List<Vocab> vocab;
final String? imageURL;
final DateTime? endAt;
final Duration? duration;
ActivityPlanModel({
required this.req,
@ -19,31 +21,70 @@ class ActivityPlanModel {
required this.instructions,
required this.vocab,
this.imageURL,
this.endAt,
this.duration,
}) : bookmarkId =
"${title.hashCode ^ learningObjective.hashCode ^ instructions.hashCode ^ imageURL.hashCode ^ vocab.map((v) => v.hashCode).reduce((a, b) => a ^ b)}";
ActivityPlanModel copyWith({
String? title,
String? learningObjective,
String? instructions,
List<Vocab>? vocab,
String? imageURL,
DateTime? endAt,
Duration? duration,
}) {
return ActivityPlanModel(
req: req,
title: title ?? this.title,
learningObjective: learningObjective ?? this.learningObjective,
instructions: instructions ?? this.instructions,
vocab: vocab ?? this.vocab,
imageURL: imageURL ?? this.imageURL,
endAt: endAt ?? this.endAt,
duration: duration ?? this.duration,
);
}
factory ActivityPlanModel.fromJson(Map<String, dynamic> json) {
return ActivityPlanModel(
imageURL: json[ModelKey.activityPlanImageURL],
instructions: json[ModelKey.activityPlanInstructions],
req: ActivityPlanRequest.fromJson(json[ModelKey.activityPlanRequest]),
title: json[ModelKey.activityPlanTitle],
learningObjective: json[ModelKey.activityPlanLearningObjective],
instructions: json[ModelKey.activityPlanInstructions],
vocab: List<Vocab>.from(
json[ModelKey.activityPlanVocab].map((vocab) => Vocab.fromJson(vocab)),
),
imageURL: json[ModelKey.activityPlanImageURL],
endAt: json[ModelKey.activityPlanEndAt] != null
? DateTime.parse(json[ModelKey.activityPlanEndAt])
: null,
duration: json[ModelKey.activityPlanDuration] != null
? Duration(
days: json[ModelKey.activityPlanDuration]['days'] ?? 0,
hours: json[ModelKey.activityPlanDuration]['hours'] ?? 0,
minutes: json[ModelKey.activityPlanDuration]['minutes'] ?? 0,
)
: null,
);
}
Map<String, dynamic> toJson() {
return {
ModelKey.activityPlanBookmarkId: bookmarkId,
ModelKey.activityPlanImageURL: imageURL,
ModelKey.activityPlanInstructions: instructions,
ModelKey.activityPlanRequest: req.toJson(),
ModelKey.activityPlanTitle: title,
ModelKey.activityPlanLearningObjective: learningObjective,
ModelKey.activityPlanInstructions: instructions,
ModelKey.activityPlanVocab: vocab.map((vocab) => vocab.toJson()).toList(),
ModelKey.activityPlanImageURL: imageURL,
ModelKey.activityPlanBookmarkId: bookmarkId,
ModelKey.activityPlanEndAt: endAt?.toIso8601String(),
ModelKey.activityPlanDuration: {
'days': duration?.inDays ?? 0,
'hours': duration?.inHours.remainder(24) ?? 0,
'minutes': duration?.inMinutes.remainder(60) ?? 0,
},
};
}

@ -163,6 +163,8 @@ class ModelKey {
static const String activityPlanVocab = "vocab";
static const String activityPlanImageURL = "image_url";
static const String activityPlanBookmarkId = "bookmark_id";
static const String activityPlanEndAt = "end_at";
static const String activityPlanDuration = "duration";
static const String activityRequestTopic = "topic";
static const String activityRequestMode = "mode";

@ -9,7 +9,6 @@ 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,57 +277,6 @@ 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 (canSendDefaultStates) {
await client.setRoomStateWithKey(
id,
@ -335,10 +284,6 @@ extension EventsRoomExtension on Room {
"",
activity.toJson(),
);
if (eventId != null) {
await setPinnedEvents([eventId]);
}
}
}

@ -1,6 +1,7 @@
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> {
@ -46,7 +47,12 @@ 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
(!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
// #Pangea
// (!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
(!AppConfig.hideUnknownEvents ||
isEventTypeKnown ||
importantStateEvents.contains(type)) &&
// Pangea#
// remove state events that we don't want to render
(isState || !AppConfig.hideAllStateEvents) &&
// #Pangea
@ -82,6 +88,7 @@ extension IsStateExtension on Event {
EventTypes.RoomMember,
EventTypes.RoomTombstone,
EventTypes.CallInvite,
PangeaEventTypes.activityPlan,
};
// Pangea#
}

Loading…
Cancel
Save