A framework for building convergent cross-platform Nextcloud clients using Flutter.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

325 lines
12 KiB

part of '../neon_spreed.dart';
class SpreedRoomPage extends StatefulWidget {
const SpreedRoomPage({
required this.bloc,
super.key,
});
final SpreedRoomBloc bloc;
@override
State<SpreedRoomPage> createState() => _SpreedRoomPageState();
}
class _SpreedRoomPageState extends State<SpreedRoomPage> {
final defaultChatTheme = const chat_ui.DefaultChatTheme();
late final chatTheme = chat_ui.DefaultChatTheme(
backgroundColor: Theme.of(context).colorScheme.background,
primaryColor: Theme.of(context).colorScheme.onBackground,
inputBackgroundColor: Theme.of(context).colorScheme.primary,
inputTextColor: Theme.of(context).colorScheme.onPrimary,
inputTextCursorColor: Theme.of(context).colorScheme.onPrimary,
receivedMessageBodyTextStyle: defaultChatTheme.receivedMessageBodyTextStyle.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
),
sentMessageBodyTextStyle: defaultChatTheme.sentMessageBodyTextStyle.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
),
unreadHeaderTheme: chat_ui.UnreadHeaderTheme(
color: Theme.of(context).colorScheme.background,
textStyle: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
);
final inputOptions = const chat_ui.InputOptions(
sendButtonVisibilityMode: chat_ui.SendButtonVisibilityMode.always,
);
late final user = chat_types.User(
id: widget.bloc.account.username,
);
void onSendPressed(final chat_types.PartialText partialText) {
widget.bloc.sendMessage(partialText.text);
}
Future<void> openCall(final spreed.Room room) async {
try {
final client = NeonProvider.of<AccountsBloc>(context).activeAccount.value!.client;
final settings = (await client.spreed.signaling.getSettings(token: widget.bloc.roomToken)).body.ocs.data;
final bloc = SpreedCallBloc(
settings,
client,
widget.bloc.roomToken,
room.sessionId,
);
if (!mounted) {
return;
}
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (final context) => SpreedCallPage(
bloc: bloc,
),
),
);
await bloc.leaveCall();
bloc.dispose();
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
if (mounted) {
NeonError.showSnackbar(context, e);
}
}
}
@override
void initState() {
super.initState();
widget.bloc.errors.listen((final error) {
NeonError.showSnackbar(context, error);
});
}
@override
Widget build(final BuildContext context) => StreamBuilder(
stream: widget.bloc.allLoaded,
builder: (final context, final allLoadedSnapshot) => ResultBuilder(
stream: widget.bloc.room,
builder: (final context, final room) => StreamBuilder(
stream: widget.bloc.lastCommonReadMessageId,
builder: (final context, final lastCommonReadMessageIdSnapshot) => StreamBuilder(
stream: widget.bloc.sendingMessage,
builder: (final context, final sendingMessageSnapshot) => ResultBuilder(
stream: widget.bloc.messages,
builder: (final context, final messages) {
final roomType = room.hasData ? spreed.RoomType.fromValue(room.requireData.type) : null;
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
titleSpacing: 0,
title: Row(
children: [
if (room.hasData) ...[
if (roomType!.isSingleUser) ...[
SpreedRoomIcon(
roomType: roomType,
roomName: room.requireData.name,
backgroundColor: Theme.of(context).colorScheme.onPrimary,
foregroundColor: Theme.of(context).colorScheme.primary,
),
const SizedBox(
width: 10,
),
],
Flexible(
child: Text(room.requireData.displayName),
),
],
if (room.error != null) ...[
const SizedBox(
width: 8,
),
Icon(
Icons.error_outline,
size: 30,
color: Theme.of(context).colorScheme.onPrimary,
),
],
if (room.isLoading) ...[
const SizedBox(
width: 8,
),
Expanded(
child: NeonLinearProgressIndicator(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
],
),
actions: [
if (room.hasData && room.requireData.readOnly == 0) ...[
if (room.requireData.hasCall) ...[
SpreedCallButton(
type: SpreedCallButtonType.joinCall,
onPressed: () async {
await openCall(room.requireData);
},
),
] else if (room.requireData.canStartCall) ...[
SpreedCallButton(
type: SpreedCallButtonType.startCall,
onPressed: () async {
await openCall(room.requireData);
},
),
],
],
],
),
body: chat_ui.Chat(
useTopSafeAreaInset: false,
showUserNames: true,
showUserAvatars: !(roomType?.isSingleUser ?? true),
theme: chatTheme,
inputOptions: inputOptions,
scrollToUnreadOptions: chat_ui.ScrollToUnreadOptions(
lastReadMessageId: room.data?.lastReadMessage.toString(),
scrollOnOpen: true,
scrollDelay: Duration.zero,
),
avatarBuilder: (final username) => NeonUserAvatar(
username: username,
account: NeonProvider.of<AccountsBloc>(context).activeAccount.value!,
),
textMessageBuilder: (
final message, {
required final messageWidth,
required final showName,
}) {
final matchers = [
if (message.metadata != null) ...[
NeonRichObject(
parameters: message.metadata!,
),
],
];
return chat_ui.TextMessage(
emojiEnlargementBehavior: chat_ui.EmojiEnlargementBehavior.multi,
hideBackgroundOnEmojiMessages: true,
message: message,
showName: showName,
usePreviewData: true,
options: chat_ui.TextMessageOptions(
matchers: matchers,
),
);
},
systemMessageBuilder: (final message) {
final matchers = [
if (message.metadata != null) ...[
NeonRichObject(
parameters: message.metadata!,
),
],
];
return chat_ui.SystemMessage(
message: message.text,
options: chat_ui.TextMessageOptions(
matchers: matchers,
),
);
},
customBottomWidget: Column(
children: [
NeonError(
messages.error,
onRetry: () async {
await widget.bloc.refresh();
},
),
if (messages.isLoading) ...[
const NeonLinearProgressIndicator(
margin: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
),
],
if ((room.data?.readOnly ?? 0) == 0) ...[
chat_ui.Input(
onSendPressed: onSendPressed,
options: inputOptions,
),
],
],
),
user: user,
onEndReached: () async {
await widget.bloc.loadMoreMessages();
},
onSendPressed: onSendPressed,
isLastPage: allLoadedSnapshot.data ?? false,
messages: [
if (sendingMessageSnapshot.hasData) ...[
chat_types.TextMessage(
id: 'sending',
author: user,
text: sendingMessageSnapshot.data!,
showStatus: true,
status: chat_types.Status.sending,
),
],
if (messages.hasData) ...[
...messages.requireData
.map(
(final message) => _spreedMessageToChatMessage(
message,
lastCommonReadMessageId: lastCommonReadMessageIdSnapshot.data,
),
)
.whereNotNull(),
],
],
),
);
},
),
),
),
),
);
chat_types.Message? _spreedMessageToChatMessage(
final spreed.ChatMessageInterface message, {
final int? lastCommonReadMessageId,
}) {
final id = message.id.toString();
final author = chat_types.User(
id: message.actorId,
firstName: message.actorDisplayName,
imageUrl: message.actorId,
);
final createdAt = message.timestamp * 1000;
// TODO: Doesn't work yet in the UI. See https://github.com/flyerhq/flutter_chat_ui/pull/256
final repliedMessage = message is spreed.ChatMessageWithParent && message.parent != null
? _spreedMessageToChatMessage(message.parent!)
: null;
final status = lastCommonReadMessageId != null && lastCommonReadMessageId >= message.id
? chat_types.Status.seen
: chat_types.Status.sent;
final metadata = message.messageParameters is Map ? message.messageParameters as Map<String, dynamic> : null;
switch (spreed.MessageType.values.byName(message.messageType)) {
case spreed.MessageType.comment:
return chat_types.TextMessage(
id: id,
author: author,
createdAt: createdAt,
repliedMessage: repliedMessage,
text: message.message,
showStatus: true,
status: status,
metadata: metadata,
);
case spreed.MessageType.command:
case spreed.MessageType.system:
return chat_types.SystemMessage(
id: id,
createdAt: createdAt,
text: message.message,
metadata: metadata,
);
default:
return null;
}
}
}