Files
Client/lib/view/pages/talk/widgets/chat_tile.dart
T

252 lines
8.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/marianumcloud/talk/actions/talk_actions.dart';
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../../api/marianumcloud/talk/set_read_marker/set_read_marker.dart';
import '../../../../extensions/date_time.dart';
import '../../../../model/account_data.dart';
import '../../../../notification/notification_tasks.dart';
import '../../../../routing/app_routes.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
import '../../../../utils/haptics.dart';
import '../../../../widget/a11y/a11y_labels.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/debug_tile.dart';
import '../../../../widget/details_bottom_sheet.dart';
import '../../../../widget/user_avatar.dart';
import '../talk_navigator.dart';
class ChatTile extends StatefulWidget {
final GetRoomResponseObject data;
final bool disableContextActions;
final bool hasDraft;
/// When set, replaces the default tap-into-chat behaviour. Used by the
/// share-intent picker to surface the room selection without opening the
/// chat view itself.
final void Function(GetRoomResponseObject room)? onTapOverride;
const ChatTile({
super.key,
required this.data,
this.disableContextActions = false,
this.hasDraft = false,
this.onTapOverride,
});
@override
State<ChatTile> createState() => _ChatTileState();
}
class _ChatTileState extends State<ChatTile> {
String? selfUsername;
@override
void initState() {
super.initState();
AccountData().waitForPopulation().then((_) {
if (!mounted) return;
setState(
() => selfUsername = AccountData().isPopulated()
? AccountData().getUsername()
: null,
);
});
}
void _refreshList() => context.read<ChatListBloc>().refresh();
Future<void> _setCurrentAsRead() async {
final token = widget.data.token;
final lastId = widget.data.lastMessage.id;
context.read<ChatListBloc>().markRoomAsRead(token, lastId);
unawaited(NotificationTasks.clearNotificationsForChat(token));
await context.read<ChatBloc>().sendServerReadMarker(token, lastId);
if (!mounted) return;
_refreshList();
}
@override
Widget build(BuildContext context) {
final chatBloc = context.watch<ChatBloc>();
final isGroup =
widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
final circleAvatar = UserAvatar(
id: isGroup ? widget.data.token : widget.data.name,
isGroup: isGroup,
);
return ListTile(
style: ListTileStyle.list,
tileColor:
chatBloc.state.data?.currentToken == widget.data.token &&
TalkNavigator.isSecondaryVisible(context)
? Theme.of(context).primaryColor.withAlpha(100)
: null,
leading: Stack(
children: [
// Der Name steht bereits im Titel der Zeile das Avatarbild selbst
// muss der Screenreader nicht ansagen. Nur bei Gruppen ist der
// Hinweis „Gruppe" nützlich (sonst nicht vom Einzelchat zu
// unterscheiden).
Semantics(
label: isGroup ? A11yLabels.group : null,
child: ExcludeSemantics(child: circleAvatar),
),
Visibility(
visible: widget.data.isFavorite,
child: Positioned(
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withAlpha(200),
borderRadius: BorderRadius.circular(90.0),
),
child: const Icon(
Icons.star,
color: Colors.amberAccent,
size: 15,
semanticLabel: A11yLabels.favorite,
),
),
),
),
],
),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
widget.data.displayName,
overflow: TextOverflow.ellipsis,
),
),
if (widget.hasDraft) ...[
const SizedBox(width: 5),
const Icon(
Icons.edit_outlined,
size: 15,
semanticLabel: A11yLabels.draft,
),
],
],
),
subtitle: Text(
'${DateTime.fromMillisecondsSinceEpoch(widget.data.lastMessage.timestamp * 1000).formatRelative()}: '
'${RichObjectStringProcessor.parseToString(widget.data.lastMessage.message.replaceAll("\n", " "), widget.data.lastMessage.messageParameters)}',
overflow: TextOverflow.ellipsis,
),
trailing: widget.data.unreadMessages <= 0
? null
: Semantics(
label: '${widget.data.unreadMessages} ${A11yLabels.unread}',
child: ExcludeSemantics(
child: Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
'${widget.data.unreadMessages}',
style: const TextStyle(color: Colors.white, fontSize: 15),
textAlign: TextAlign.center,
),
),
),
),
onTap: () {
if (widget.onTapOverride != null) {
widget.onTapOverride!(widget.data);
return;
}
if (selfUsername == null) return;
// openChatView is the single entry point for opening a chat —
// it handles optimistic mark-as-read, tray cleanup, push, and
// setToken in one place so the notification-tap path gets the
// same treatment as a tile tap.
AppRoutes.openChatView(
context,
room: widget.data,
selfId: selfUsername!,
avatar: circleAvatar,
overrideToSingleSubScreen: true,
);
},
onLongPress: () {
if (widget.disableContextActions) return;
Haptics.longPress();
showDetailsBottomSheet(
context,
children: (sheetCtx) => [
if (widget.data.unreadMessages > 0)
AsyncListTile(
leading: const Icon(Icons.mark_chat_read_outlined),
title: const Text('Als gelesen markieren'),
onPressed: _setCurrentAsRead,
)
else
AsyncListTile(
leading: const Icon(Icons.mark_chat_unread_outlined),
title: const Text('Als ungelesen markieren'),
onPressed: () async {
await SetReadMarker(widget.data.token, false).run();
if (mounted) _refreshList();
},
),
if (widget.data.isFavorite)
AsyncListTile(
leading: const Icon(Icons.stars_outlined),
title: const Text('Von Favoriten entfernen'),
onPressed: () async {
await SetFavorite(widget.data.token, false).run();
if (mounted) _refreshList();
},
)
else
AsyncListTile(
leading: const Icon(Icons.star_outline),
title: const Text('Zu Favoriten hinzufügen'),
onPressed: () async {
await SetFavorite(widget.data.token, true).run();
if (mounted) _refreshList();
},
),
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Talk-Chat verlassen'),
onTap: () {
Navigator.of(sheetCtx).pop();
ConfirmDialog(
title: 'Talk-Chat verlassen',
content:
'Du benötigst ggf. eine Einladung um erneut beizutreten.',
confirmButton: 'Verlassen',
onConfirmAsync: () async {
await LeaveRoom(widget.data.token).run();
if (mounted) _refreshList();
},
).asDialog(context);
},
),
DebugTile(sheetCtx).jsonData(widget.data.toJson()),
],
);
},
);
}
}