Merge pull request #3074 from pangeachat/3050-activity-duration-in-chat-activity-planner-end-flow
feat: initial work for add duration to in-chat activitiespull/2245/head
commit
c1fad2c05e
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue