fluffychat/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart

394 lines
18 KiB
Dart
Raw Normal View History

2021-10-26 16:50:34 +00:00
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
2021-10-26 16:50:34 +00:00
2024-04-14 13:49:46 +00:00
import 'package:fluffychat/config/app_config.dart';
2023-11-11 16:56:23 +00:00
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/url_launcher.dart';
2023-01-07 09:29:34 +00:00
import 'package:fluffychat/widgets/avatar.dart';
2023-11-11 16:56:23 +00:00
import 'package:fluffychat/widgets/presence_builder.dart';
import '../../widgets/matrix.dart';
2021-11-09 20:32:16 +00:00
import 'user_bottom_sheet.dart';
2021-05-22 07:13:47 +00:00
class UserBottomSheetView extends StatelessWidget {
final UserBottomSheetController controller;
const UserBottomSheetView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final user = controller.widget.user;
2023-08-11 05:55:15 +00:00
final userId = (user?.id ?? controller.widget.profile?.userId)!;
final displayname = (user?.calcDisplayname() ??
controller.widget.profile?.displayName ??
controller.widget.profile?.userId.localpart)!;
final avatarUrl = user?.avatarUrl ?? controller.widget.profile?.avatarUrl;
2023-08-13 16:27:57 +00:00
final client = Matrix.of(controller.widget.outerContext).client;
2023-08-11 05:55:15 +00:00
final profileSearchError = controller.widget.profileSearchError;
final dmRoomId = client.getDirectChatFromUserId(userId);
2023-01-07 09:29:34 +00:00
return SafeArea(
child: Scaffold(
appBar: AppBar(
leading: CloseButton(
onPressed: Navigator.of(context, rootNavigator: false).pop,
),
2023-12-22 19:18:51 +00:00
centerTitle: false,
2024-07-26 14:50:41 +00:00
title: Text(displayname),
actions: dmRoomId == null
? null
: [
Padding(
2024-07-26 14:20:13 +00:00
padding: const EdgeInsets.symmetric(horizontal: 8),
child: FloatingActionButton.small(
elevation: 0,
onPressed: () => controller
.participantAction(UserBottomSheetAction.message),
child: const Icon(Icons.chat_outlined),
),
),
],
2023-01-07 09:29:34 +00:00
),
body: StreamBuilder<Object>(
stream: user?.room.client.onSync.stream.where(
(syncUpdate) =>
syncUpdate.rooms?.join?[user.room.id]?.timeline?.events?.any(
(state) => state.type == EventTypes.RoomPowerLevels,
) ??
false,
),
builder: (context, snapshot) {
final theme = Theme.of(context);
return ListView(
children: [
if (user?.membership == Membership.knock)
Padding(
padding: const EdgeInsets.all(12.0),
child: Material(
2024-05-16 07:05:04 +00:00
color:
2024-05-29 17:30:53 +00:00
// ignore: deprecated_member_use
theme.colorScheme.surfaceVariant,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
child: ListTile(
minVerticalPadding: 16,
title: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
L10n.of(context)
.userWouldLikeToChangeTheChat(displayname),
2024-04-14 13:49:46 +00:00
),
),
subtitle: Row(
children: [
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.primary,
),
onPressed: controller.knockAccept,
icon: const Icon(Icons.check_outlined),
label: Text(L10n.of(context).accept),
),
const SizedBox(width: 12),
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor:
theme.colorScheme.errorContainer,
foregroundColor:
theme.colorScheme.onErrorContainer,
),
onPressed: controller.knockDecline,
icon: const Icon(Icons.cancel_outlined),
label: Text(L10n.of(context).decline),
),
],
2024-04-14 13:49:46 +00:00
),
),
2024-04-14 13:49:46 +00:00
),
),
Row(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Avatar(
2024-07-26 16:31:27 +00:00
client:
Matrix.of(controller.widget.outerContext).client,
mxContent: avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
2023-08-11 05:55:15 +00:00
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: () => FluffyShare.share(
userId,
context,
copyOnly: true,
),
icon: const Icon(
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.onSurface,
),
label: Text(
userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
2024-07-26 14:50:41 +00:00
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
if (presence == null ||
(presence.presence == PresenceType.offline &&
presence.lastActiveTimestamp == null)) {
return const SizedBox.shrink();
}
final dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.grey;
final lastActiveTimestamp =
presence.lastActiveTimestamp;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 16),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
),
),
const SizedBox(width: 12),
if (presence.currentlyActive == true)
Text(
L10n.of(context).currentlyActive,
2024-07-26 14:50:41 +00:00
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
2024-07-26 14:50:41 +00:00
)
else if (lastActiveTimestamp != null)
Text(
L10n.of(context).lastActiveAgo(
2024-07-26 14:50:41 +00:00
lastActiveTimestamp
.localizedTimeShort(context),
),
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
2024-07-26 14:50:41 +00:00
),
],
);
},
),
],
2023-08-11 05:55:15 +00:00
),
),
],
),
2024-07-26 15:32:36 +00:00
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
final status = presence?.statusMsg;
if (status == null || status.isEmpty) {
return const SizedBox.shrink();
}
return ListTile(
title: SelectableLinkify(
text: status,
style: const TextStyle(fontSize: 16),
options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle(
color: Colors.blueAccent,
decorationColor: Colors.blueAccent,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
);
},
),
if (userId != client.userID)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: dmRoomId == null
? ElevatedButton.icon(
onPressed: () => controller.participantAction(
UserBottomSheetAction.message,
),
icon: const Icon(Icons.chat_outlined),
label: Text(L10n.of(context).startConversation),
)
: TextField(
controller: controller.sendController,
readOnly: controller.isSending,
onSubmitted: controller.sendAction,
minLines: 1,
maxLines: 1,
textInputAction: TextInputAction.send,
decoration: InputDecoration(
errorText: controller.sendError
?.toLocalizedString(context),
hintText: L10n.of(context).sendMessages,
2024-07-26 14:20:13 +00:00
suffix: controller.isSending
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
)
2024-07-26 14:20:13 +00:00
: null,
suffixIcon: controller.isSending
? null
: IconButton(
icon: const Icon(Icons.send_outlined),
onPressed: controller.sendAction,
),
),
),
),
if (controller.widget.onMention != null)
ListTile(
leading: const Icon(Icons.alternate_email_outlined),
title: Text(L10n.of(context).mention),
onTap: () => controller
.participantAction(UserBottomSheetAction.mention),
),
if (user != null) ...[
Divider(color: theme.dividerColor),
ListTile(
title: Text(
'${L10n.of(context).userRole} (${user.powerLevel})',
),
leading: const Icon(Icons.person_outlined),
2024-04-15 08:12:37 +00:00
trailing: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2),
color: theme.colorScheme.onInverseSurface,
2024-04-15 08:12:37 +00:00
child: DropdownButton<int>(
onChanged: user.canChangeUserPowerLevel ||
// Workaround until https://github.com/famedly/matrix-dart-sdk/pull/1765
(user.room.canChangePowerLevel &&
user.id == user.room.client.userID)
2024-04-15 08:12:37 +00:00
? controller.setPowerLevel
: null,
value: {0, 50, 100}.contains(user.powerLevel)
? user.powerLevel
: null,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2),
underline: const SizedBox.shrink(),
items: [
DropdownMenuItem(
value: 0,
child: Text(L10n.of(context).user),
2024-04-15 08:12:37 +00:00
),
DropdownMenuItem(
value: 50,
child: Text(L10n.of(context).moderator),
2024-04-15 08:12:37 +00:00
),
DropdownMenuItem(
value: 100,
child: Text(L10n.of(context).admin),
2024-04-15 08:12:37 +00:00
),
DropdownMenuItem(
value: null,
child: Text(L10n.of(context).custom),
2024-04-15 08:12:37 +00:00
),
],
),
),
),
],
Divider(color: theme.dividerColor),
if (user != null && user.canKick)
ListTile(
textColor: theme.colorScheme.error,
iconColor: theme.colorScheme.error,
title: Text(L10n.of(context).kickFromChat),
leading: const Icon(Icons.exit_to_app_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.kick),
),
if (user != null &&
user.canBan &&
user.membership != Membership.ban)
ListTile(
textColor: theme.colorScheme.onErrorContainer,
iconColor: theme.colorScheme.onErrorContainer,
title: Text(L10n.of(context).banFromChat),
leading: const Icon(Icons.warning_sharp),
onTap: () =>
controller.participantAction(UserBottomSheetAction.ban),
)
else if (user != null &&
user.canBan &&
user.membership == Membership.ban)
ListTile(
title: Text(L10n.of(context).unbanFromChat),
leading: const Icon(Icons.warning_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.unban),
),
if (user != null && user.id != client.userID)
ListTile(
textColor: theme.colorScheme.onErrorContainer,
iconColor: theme.colorScheme.onErrorContainer,
title: Text(L10n.of(context).reportUser),
leading: const Icon(Icons.gavel_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.report),
),
if (profileSearchError != null)
ListTile(
leading: const Icon(
Icons.warning_outlined,
color: Colors.orange,
),
subtitle: Text(
L10n.of(context).profileNotFound,
style: const TextStyle(color: Colors.orange),
2023-12-23 11:14:51 +00:00
),
),
if (userId != client.userID &&
!client.ignoredUsers.contains(userId))
ListTile(
textColor: theme.colorScheme.onErrorContainer,
iconColor: theme.colorScheme.onErrorContainer,
leading: const Icon(Icons.block_outlined),
title: Text(L10n.of(context).block),
onTap: () => controller
.participantAction(UserBottomSheetAction.ignore),
),
],
);
},
),
),
);
}
}