dart format
This commit is contained in:
@@ -23,7 +23,8 @@ class ChatList extends StatelessWidget {
|
||||
const ChatList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocModule<ChatListBloc, LoadableState<ChatListState>>(
|
||||
Widget build(BuildContext context) =>
|
||||
BlocModule<ChatListBloc, LoadableState<ChatListState>>(
|
||||
create: (_) => ChatListBloc(),
|
||||
child: (context, bloc, _) => const _ChatListView(),
|
||||
);
|
||||
@@ -83,16 +84,22 @@ class _ChatListViewState extends State<_ChatListView> {
|
||||
|
||||
void _maybeAskForNotificationPermission() {
|
||||
final notificationSettings = _settings.val().notificationSettings;
|
||||
if (notificationSettings.enabled || notificationSettings.askUsageDismissed) return;
|
||||
if (notificationSettings.enabled ||
|
||||
notificationSettings.askUsageDismissed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.val(write: true).notificationSettings.askUsageDismissed = true;
|
||||
ConfirmDialog(
|
||||
icon: Icons.notifications_active_outlined,
|
||||
title: 'Benachrichtigungen aktivieren',
|
||||
content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
|
||||
content:
|
||||
'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
|
||||
confirmButton: 'Weiter',
|
||||
onConfirm: () {
|
||||
FirebaseMessaging.instance.requestPermission(provisional: false).then((value) {
|
||||
FirebaseMessaging.instance.requestPermission(provisional: false).then((
|
||||
value,
|
||||
) {
|
||||
if (!mounted) return;
|
||||
switch (value.authorizationStatus) {
|
||||
case AuthorizationStatus.authorized:
|
||||
@@ -129,7 +136,10 @@ class _ChatListViewState extends State<_ChatListView> {
|
||||
onPressed: () {
|
||||
final rooms = bloc.state.data?.rooms;
|
||||
if (rooms == null) return;
|
||||
showSearch(context: context, delegate: SearchChat(rooms.data.toList()));
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: SearchChat(rooms.data.toList()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -138,11 +148,14 @@ class _ChatListViewState extends State<_ChatListView> {
|
||||
heroTag: 'createChat',
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
onPressed: () {
|
||||
showSearch(context: context, delegate: JoinChat()).then((username) {
|
||||
showSearch(context: context, delegate: JoinChat()).then((
|
||||
username,
|
||||
) {
|
||||
if (username == null || !context.mounted) return;
|
||||
ConfirmDialog(
|
||||
title: 'Chat starten',
|
||||
content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
|
||||
content:
|
||||
"Möchtest du einen Chat mit Nutzer '$username' starten?",
|
||||
confirmButton: 'Chat starten',
|
||||
onConfirmAsync: () => bloc.createDirectChat(username),
|
||||
).asDialog(context);
|
||||
@@ -155,7 +168,10 @@ class _ChatListViewState extends State<_ChatListView> {
|
||||
final rooms = state.rooms;
|
||||
if (rooms == null) return const SizedBox.shrink();
|
||||
|
||||
final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
|
||||
final talkSettings = context
|
||||
.watch<SettingsCubit>()
|
||||
.val()
|
||||
.talkSettings;
|
||||
final sorted = rooms.sortBy(
|
||||
lastActivity: true,
|
||||
favoritesToTop: talkSettings.sortFavoritesToTop,
|
||||
@@ -172,7 +188,11 @@ class _ChatListViewState extends State<_ChatListView> {
|
||||
return ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: sorted.map((room) {
|
||||
final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token);
|
||||
final hasDraft = _settings
|
||||
.val()
|
||||
.talkSettings
|
||||
.drafts
|
||||
.containsKey(room.token);
|
||||
return ChatTile(data: room, hasDraft: hasDraft);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
@@ -20,7 +20,12 @@ class ChatView extends StatefulWidget {
|
||||
final String selfId;
|
||||
final UserAvatar avatar;
|
||||
|
||||
const ChatView({super.key, required this.room, required this.selfId, required this.avatar});
|
||||
const ChatView({
|
||||
super.key,
|
||||
required this.room,
|
||||
required this.selfId,
|
||||
required this.avatar,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatView> createState() => _ChatViewState();
|
||||
@@ -37,46 +42,58 @@ class _ChatViewState extends State<ChatView> {
|
||||
final messages = <Widget>[];
|
||||
var lastDate = DateTime.now();
|
||||
for (final element in response.sortByTimestamp()) {
|
||||
final elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000);
|
||||
final elementDate = DateTime.fromMillisecondsSinceEpoch(
|
||||
element.timestamp * 1000,
|
||||
);
|
||||
|
||||
if (element.systemMessage.contains('reaction')) continue;
|
||||
if (element.systemMessage.contains('poll_voted')) continue;
|
||||
final commonRead = int.parse(response.headers?['x-chat-last-common-read'] ?? '0');
|
||||
final commonRead = int.parse(
|
||||
response.headers?['x-chat-last-common-read'] ?? '0',
|
||||
);
|
||||
|
||||
if (!elementDate.isSameDay(lastDate)) {
|
||||
lastDate = elementDate;
|
||||
messages.add(ChatBubble(
|
||||
context: context,
|
||||
isSender: false,
|
||||
bubbleData: GetChatResponseObject.getDateDummy(element.timestamp),
|
||||
chatData: widget.room,
|
||||
refetch: ({bool renew = false}) => _refresh(),
|
||||
));
|
||||
messages.add(
|
||||
ChatBubble(
|
||||
context: context,
|
||||
isSender: false,
|
||||
bubbleData: GetChatResponseObject.getDateDummy(element.timestamp),
|
||||
chatData: widget.room,
|
||||
refetch: ({bool renew = false}) => _refresh(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
messages.add(ChatBubble(
|
||||
context: context,
|
||||
isSender: element.actorId == widget.selfId &&
|
||||
element.messageType == GetRoomResponseObjectMessageType.comment,
|
||||
bubbleData: element,
|
||||
chatData: widget.room,
|
||||
refetch: ({bool renew = false}) => _refresh(),
|
||||
isRead: element.id <= commonRead,
|
||||
selfId: widget.selfId,
|
||||
));
|
||||
messages.add(
|
||||
ChatBubble(
|
||||
context: context,
|
||||
isSender:
|
||||
element.actorId == widget.selfId &&
|
||||
element.messageType == GetRoomResponseObjectMessageType.comment,
|
||||
bubbleData: element,
|
||||
chatData: widget.room,
|
||||
refetch: ({bool renew = false}) => _refresh(),
|
||||
isRead: element.id <= commonRead,
|
||||
selfId: widget.selfId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (response.data.length >= 200) {
|
||||
messages.insert(0, ChatBubble(
|
||||
context: context,
|
||||
isSender: false,
|
||||
bubbleData: GetChatResponseObject.getTextDummy(
|
||||
'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
|
||||
'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de',
|
||||
messages.insert(
|
||||
0,
|
||||
ChatBubble(
|
||||
context: context,
|
||||
isSender: false,
|
||||
bubbleData: GetChatResponseObject.getTextDummy(
|
||||
'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
|
||||
'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de',
|
||||
),
|
||||
chatData: widget.room,
|
||||
refetch: ({bool renew = false}) => _refresh(),
|
||||
),
|
||||
chatData: widget.room,
|
||||
refetch: ({bool renew = false}) => _refresh(),
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
return messages;
|
||||
@@ -84,52 +101,62 @@ class _ChatViewState extends State<ChatView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
backgroundColor: const Color(0xffefeae2),
|
||||
appBar: ClickableAppBar(
|
||||
onTap: () => TalkNavigator.pushSplitView(context, ChatInfo(widget.room)),
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
widget.avatar,
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(widget.room.displayName, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
),
|
||||
],
|
||||
backgroundColor: const Color(0xffefeae2),
|
||||
appBar: ClickableAppBar(
|
||||
onTap: () => TalkNavigator.pushSplitView(context, ChatInfo(widget.room)),
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
widget.avatar,
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.room.displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: const AssetImage('assets/background/chat.png'),
|
||||
scale: 1.5,
|
||||
opacity: 1,
|
||||
repeat: ImageRepeat.repeat,
|
||||
invertColors: AppTheme.isDarkMode(context),
|
||||
),
|
||||
),
|
||||
body: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: const AssetImage('assets/background/chat.png'),
|
||||
scale: 1.5,
|
||||
opacity: 1,
|
||||
repeat: ImageRepeat.repeat,
|
||||
invertColors: AppTheme.isDarkMode(context),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LoadableStateConsumer<ChatBloc, ChatState>(
|
||||
isReady: (state) =>
|
||||
state.chatResponse != null &&
|
||||
state.currentToken == widget.room.token,
|
||||
child: (state, _) => ListView(
|
||||
reverse: true,
|
||||
controller: _listController,
|
||||
children: _buildMessages(state.chatResponse!).reversed.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LoadableStateConsumer<ChatBloc, ChatState>(
|
||||
isReady: (state) =>
|
||||
state.chatResponse != null && state.currentToken == widget.room.token,
|
||||
child: (state, _) => ListView(
|
||||
reverse: true,
|
||||
controller: _listController,
|
||||
children: _buildMessages(state.chatResponse!).reversed.toList(),
|
||||
ColoredBox(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: TalkNavigator.isSecondaryVisible(context)
|
||||
? ChatTextfield(widget.room.token, selfId: widget.selfId)
|
||||
: SafeArea(
|
||||
child: ChatTextfield(
|
||||
widget.room.token,
|
||||
selfId: widget.selfId,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ColoredBox(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: TalkNavigator.isSecondaryVisible(context)
|
||||
? ChatTextfield(widget.room.token, selfId: widget.selfId)
|
||||
: SafeArea(child: ChatTextfield(widget.room.token, selfId: widget.selfId)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ extension ColorExtensions on Color {
|
||||
final invertedR = 1.0 - r;
|
||||
final invertedG = 1.0 - g;
|
||||
final invertedB = 1.0 - b;
|
||||
return Color.from(alpha: a, red: invertedR, green: invertedG, blue: invertedB);
|
||||
return Color.from(
|
||||
alpha: a,
|
||||
red: invertedR,
|
||||
green: invertedG,
|
||||
blue: invertedB,
|
||||
);
|
||||
}
|
||||
|
||||
Color withWhite(int whiteValue) {
|
||||
@@ -23,14 +28,18 @@ class ChatBubbleStyles {
|
||||
ChatBubbleStyles(this.context);
|
||||
|
||||
BubbleStyle getSystemStyle() => BubbleStyle(
|
||||
color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white,
|
||||
color: AppTheme.isDarkMode(context)
|
||||
? const Color(0xff182229)
|
||||
: Colors.white,
|
||||
elevation: 2,
|
||||
margin: const BubbleEdges.only(bottom: 20, top: 10),
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
|
||||
BubbleStyle getRemoteStyle(bool seamless) {
|
||||
var color = AppTheme.isDarkMode(context) ? const Color(0xff202c33) : Colors.white;
|
||||
var color = AppTheme.isDarkMode(context)
|
||||
? const Color(0xff202c33)
|
||||
: Colors.white;
|
||||
return BubbleStyle(
|
||||
nip: BubbleNip.leftTop,
|
||||
color: seamless ? Colors.transparent : color,
|
||||
@@ -41,7 +50,9 @@ class ChatBubbleStyles {
|
||||
}
|
||||
|
||||
BubbleStyle getSelfStyle(bool seamless) {
|
||||
var color = AppTheme.isDarkMode(context) ? const Color(0xff005c4b) : const Color(0xffd3d3d3);
|
||||
var color = AppTheme.isDarkMode(context)
|
||||
? const Color(0xff005c4b)
|
||||
: const Color(0xffd3d3d3);
|
||||
return BubbleStyle(
|
||||
nip: BubbleNip.rightBottom,
|
||||
color: seamless ? Colors.transparent : color,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
@@ -19,20 +18,19 @@ class ChatMessage {
|
||||
bool get containsFile => file != null;
|
||||
|
||||
ChatMessage({required this.originalMessage, this.originalData}) {
|
||||
if(originalData?.containsKey('file') ?? false) {
|
||||
if (originalData?.containsKey('file') ?? false) {
|
||||
file = originalData?['file'];
|
||||
}
|
||||
content = RichObjectStringProcessor.parseToString(originalMessage, originalData);
|
||||
content = RichObjectStringProcessor.parseToString(
|
||||
originalMessage,
|
||||
originalData,
|
||||
);
|
||||
}
|
||||
|
||||
Widget getWidget() {
|
||||
var contentWidget = Linkify(text: content, onOpen: UrlOpener.onOpen);
|
||||
|
||||
var contentWidget = Linkify(
|
||||
text: content,
|
||||
onOpen: UrlOpener.onOpen,
|
||||
);
|
||||
|
||||
if(originalData?['object']?.type == RichObjectStringObjectType.talkPoll) {
|
||||
if (originalData?['object']?.type == RichObjectStringObjectType.talkPoll) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.poll_outlined),
|
||||
title: Text(originalData!['object']!.name),
|
||||
@@ -40,38 +38,49 @@ class ChatMessage {
|
||||
);
|
||||
}
|
||||
|
||||
if(file == null) return contentWidget;
|
||||
if (file == null) return contentWidget;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
errorWidget: (context, url, error) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.file_open_outlined, size: 35),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(child: Text(file!.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
placeholder: (context, url) => const Padding(padding: EdgeInsets.all(15), child: SizedBox(width: 50, child: LinearProgressIndicator())),
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
errorListener: (value) {},
|
||||
httpHeaders: AccountData().authHeaders(),
|
||||
imageUrl: 'https://${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=130&y=-1&a=1',
|
||||
padding: const EdgeInsets.only(top: 5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
errorWidget: (context, url, error) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.file_open_outlined, size: 35),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
file!.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
if(originalMessage != '{file}') ...[
|
||||
SizedBox(height: 5),
|
||||
contentWidget
|
||||
]
|
||||
alignment: Alignment.center,
|
||||
placeholder: (context, url) => const Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: SizedBox(width: 50, child: LinearProgressIndicator()),
|
||||
),
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
errorListener: (value) {},
|
||||
httpHeaders: AccountData().authHeaders(),
|
||||
imageUrl:
|
||||
'https://${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=130&y=-1&a=1',
|
||||
),
|
||||
if (originalMessage != '{file}') ...[
|
||||
SizedBox(height: 5),
|
||||
contentWidget,
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,18 +28,17 @@ class _ChatInfoState extends State<ChatInfo> {
|
||||
setState(() {
|
||||
participants = data;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var isGroup = widget.room.type != GetRoomResponseObjectConversationType.oneToOne;
|
||||
var isGroup =
|
||||
widget.room.type != GetRoomResponseObjectConversationType.oneToOne;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.room.displayName),
|
||||
),
|
||||
appBar: AppBar(title: Text(widget.room.displayName)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -52,23 +51,34 @@ class _ChatInfoState extends State<ChatInfo> {
|
||||
size: 80,
|
||||
),
|
||||
onTap: () {
|
||||
if(isGroup) return;
|
||||
TalkNavigator.pushSplitView(context, LargeProfilePictureView(widget.room.name));
|
||||
if (isGroup) return;
|
||||
TalkNavigator.pushSplitView(
|
||||
context,
|
||||
LargeProfilePictureView(widget.room.name),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Text(widget.room.displayName, textAlign: TextAlign.center, style: const TextStyle(fontSize: 30)),
|
||||
if(!isGroup) Text(widget.room.name),
|
||||
Text(
|
||||
widget.room.displayName,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 30),
|
||||
),
|
||||
if (!isGroup) Text(widget.room.name),
|
||||
const SizedBox(height: 10),
|
||||
if(isGroup) Text(widget.room.description, textAlign: TextAlign.center),
|
||||
if (isGroup)
|
||||
Text(widget.room.description, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 30),
|
||||
if(participants == null) const LoadingSpinner(),
|
||||
if(participants != null) ...[
|
||||
if (participants == null) const LoadingSpinner(),
|
||||
if (participants != null) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.supervised_user_circle),
|
||||
title: Text('${participants!.data.length} Mitglieder'),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () => TalkNavigator.pushSplitView(context, ParticipantsListView(participants!)),
|
||||
onTap: () => TalkNavigator.pushSplitView(
|
||||
context,
|
||||
ParticipantsListView(participants!),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -13,7 +13,11 @@ import '../../../../widget/user_avatar.dart';
|
||||
class MessageReactions extends StatefulWidget {
|
||||
final String token;
|
||||
final int messageId;
|
||||
const MessageReactions({super.key, required this.token, required this.messageId});
|
||||
const MessageReactions({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.messageId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MessageReactions> createState() => _MessageReactionsState();
|
||||
@@ -25,53 +29,67 @@ class _MessageReactionsState extends State<MessageReactions> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
data = GetReactions(chatToken: widget.token, messageId: widget.messageId).run();
|
||||
data = GetReactions(
|
||||
chatToken: widget.token,
|
||||
messageId: widget.messageId,
|
||||
).run();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Reaktionen'),
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: data,
|
||||
builder: (context, snapshot) {
|
||||
if(snapshot.connectionState == ConnectionState.waiting) return const LoadingSpinner();
|
||||
if(snapshot.data!.data.isEmpty) return const PlaceholderView(icon: Icons.search_off_outlined, text: 'Keine Reaktionen gefunden!');
|
||||
return ListView(
|
||||
children: [
|
||||
...snapshot.data!.data.entries.map<Widget>((entry) => ExpansionTile(
|
||||
textColor: Theme.of(context).colorScheme.onSurface,
|
||||
collapsedTextColor: Theme.of(context).colorScheme.onSurface,
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
collapsedIconColor: Theme.of(context).colorScheme.onSurface,
|
||||
appBar: AppBar(title: const Text('Reaktionen')),
|
||||
body: FutureBuilder(
|
||||
future: data,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const LoadingSpinner();
|
||||
}
|
||||
if (snapshot.data!.data.isEmpty) {
|
||||
return const PlaceholderView(
|
||||
icon: Icons.search_off_outlined,
|
||||
text: 'Keine Reaktionen gefunden!',
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
children: [
|
||||
...snapshot.data!.data.entries.map<Widget>(
|
||||
(entry) => ExpansionTile(
|
||||
textColor: Theme.of(context).colorScheme.onSurface,
|
||||
collapsedTextColor: Theme.of(context).colorScheme.onSurface,
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
collapsedIconColor: Theme.of(context).colorScheme.onSurface,
|
||||
|
||||
subtitle: const Text('Tippe für mehr'),
|
||||
leading: CenteredLeading(Text(entry.key)),
|
||||
title: Text('${entry.value.length} mal reagiert'),
|
||||
children: entry.value.map((e) {
|
||||
var isSelf = AccountData().getUsername() == e.actorId;
|
||||
return ListTile(
|
||||
leading: UserAvatar(id: e.actorId, isGroup: false),
|
||||
title: Text(e.actorDisplayName),
|
||||
subtitle: isSelf
|
||||
subtitle: const Text('Tippe für mehr'),
|
||||
leading: CenteredLeading(Text(entry.key)),
|
||||
title: Text('${entry.value.length} mal reagiert'),
|
||||
children: entry.value.map((e) {
|
||||
var isSelf = AccountData().getUsername() == e.actorId;
|
||||
return ListTile(
|
||||
leading: UserAvatar(id: e.actorId, isGroup: false),
|
||||
title: Text(e.actorDisplayName),
|
||||
subtitle: isSelf
|
||||
? const Text('Du')
|
||||
: e.actorType == GetReactionsResponseObjectActorType.guests ? const Text('Gast') : null,
|
||||
trailing: isSelf
|
||||
: e.actorType ==
|
||||
GetReactionsResponseObjectActorType.guests
|
||||
? const Text('Gast')
|
||||
: null,
|
||||
trailing: isSelf
|
||||
? null
|
||||
: Visibility(
|
||||
visible: kReleaseMode,
|
||||
child: IconButton(
|
||||
onPressed: () => UnimplementedDialog.show(context),
|
||||
icon: const Icon(Icons.textsms_outlined),
|
||||
visible: kReleaseMode,
|
||||
child: IconButton(
|
||||
onPressed: () =>
|
||||
UnimplementedDialog.show(context),
|
||||
icon: const Icon(Icons.textsms_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
))
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,38 +10,46 @@ class ParticipantsListView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String lastname(participant) => participant.displayName.toString().split(' ').last;
|
||||
|
||||
final participants = participantsResponse.data
|
||||
.sorted((a, b) {
|
||||
final typeComparison = a.participantType.index.compareTo(b.participantType.index);
|
||||
if (typeComparison != 0) return typeComparison;
|
||||
return lastname(a).compareTo(lastname(b));
|
||||
});
|
||||
var groupedParticipants = participants.groupListsBy((participant) => participant.participantType);
|
||||
String lastname(participant) =>
|
||||
participant.displayName.toString().split(' ').last;
|
||||
|
||||
final participants = participantsResponse.data.sorted((a, b) {
|
||||
final typeComparison = a.participantType.index.compareTo(
|
||||
b.participantType.index,
|
||||
);
|
||||
if (typeComparison != 0) return typeComparison;
|
||||
return lastname(a).compareTo(lastname(b));
|
||||
});
|
||||
var groupedParticipants = participants.groupListsBy(
|
||||
(participant) => participant.participantType,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Mitglieder'),
|
||||
),
|
||||
appBar: AppBar(title: const Text('Mitglieder')),
|
||||
body: ListView(
|
||||
children: [
|
||||
...groupedParticipants.entries.map((entry) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(entry.key.prettyName),
|
||||
titleTextStyle: Theme.of(context).textTheme.titleMedium
|
||||
),
|
||||
...entry.value.map((participant) => ListTile(
|
||||
leading: UserAvatar(id: participant.actorId),
|
||||
title: Text(participant.displayName),
|
||||
subtitle: participant.statusMessage != null ? Text(participant.statusMessage!) : null,
|
||||
)),
|
||||
Divider(),
|
||||
],
|
||||
))
|
||||
...groupedParticipants.entries.map(
|
||||
(entry) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(entry.key.prettyName),
|
||||
titleTextStyle: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
...entry.value.map(
|
||||
(participant) => ListTile(
|
||||
leading: UserAvatar(id: participant.actorId),
|
||||
title: Text(participant.displayName),
|
||||
subtitle: participant.statusMessage != null
|
||||
? Text(participant.statusMessage!)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -14,10 +13,11 @@ class JoinChat extends SearchDelegate<String> {
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) => [
|
||||
if(future != null && query.isNotEmpty) FutureBuilder(
|
||||
if (future != null && query.isNotEmpty)
|
||||
FutureBuilder(
|
||||
future: future!.value,
|
||||
builder: (context, snapshot) {
|
||||
if(snapshot.connectionState != ConnectionState.done) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Center(child: AppProgressIndicator.medium()),
|
||||
@@ -26,17 +26,18 @@ class JoinChat extends SearchDelegate<String> {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
if(query.isNotEmpty) IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)),
|
||||
];
|
||||
if (query.isNotEmpty)
|
||||
IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget? buildLeading(BuildContext context) => null;
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
if(future != null) future!.cancel();
|
||||
if (future != null) future!.cancel();
|
||||
|
||||
if(query.isEmpty) {
|
||||
if (query.isEmpty) {
|
||||
return const PlaceholderView(
|
||||
text: 'Suche nach benutzern',
|
||||
icon: Icons.person_search_outlined,
|
||||
@@ -47,13 +48,15 @@ class JoinChat extends SearchDelegate<String> {
|
||||
return FutureBuilder<AutocompleteResponse>(
|
||||
future: future!.value,
|
||||
builder: (context, snapshot) {
|
||||
if(snapshot.hasData) {
|
||||
if (snapshot.hasData) {
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.data.length,
|
||||
itemBuilder: (context, index) {
|
||||
var object = snapshot.data!.data[index];
|
||||
var circleAvatar = CircleAvatar(
|
||||
foregroundImage: Image.network('https://${EndpointData().nextcloud().full()}/avatar/${object.id}/128').image,
|
||||
foregroundImage: Image.network(
|
||||
'https://${EndpointData().nextcloud().full()}/avatar/${object.id}/128',
|
||||
).image,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
child: const Icon(Icons.person),
|
||||
@@ -67,9 +70,9 @@ class JoinChat extends SearchDelegate<String> {
|
||||
close(context, object.id);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if(snapshot.hasError) {
|
||||
} else if (snapshot.hasError) {
|
||||
return PlaceholderView(
|
||||
icon: Icons.search_off,
|
||||
text: errorToUserMessage(snapshot.error),
|
||||
@@ -83,5 +86,4 @@ class JoinChat extends SearchDelegate<String> {
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) => buildResults(context);
|
||||
|
||||
}
|
||||
|
||||
@@ -10,17 +10,26 @@ class SearchChat extends SearchDelegate<GetRoomResponseObject?> {
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) => [
|
||||
if(query.isNotEmpty) IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)),
|
||||
];
|
||||
if (query.isNotEmpty)
|
||||
IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget? buildLeading(BuildContext context) => null;
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
var items = chats.where(
|
||||
(e) => e.displayName.toString().toLowerCase().contains(query.toLowerCase()) || e.name.toString().toLowerCase().contains(query.toLowerCase())
|
||||
).toList()..sort((a, b) => b.lastActivity.compareTo(a.lastActivity));
|
||||
var items =
|
||||
chats
|
||||
.where(
|
||||
(e) =>
|
||||
e.displayName.toString().toLowerCase().contains(
|
||||
query.toLowerCase(),
|
||||
) ||
|
||||
e.name.toString().toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => b.lastActivity.compareTo(a.lastActivity));
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_split_view/flutter_split_view.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
class TalkNavigator {
|
||||
static bool hasSplitViewState(BuildContext context) => context.findAncestorStateOfType<SplitViewState>() != null;
|
||||
static bool isSecondaryVisible(BuildContext context) => hasSplitViewState(context) && SplitView.of(context).isSecondaryVisible;
|
||||
static bool hasSplitViewState(BuildContext context) =>
|
||||
context.findAncestorStateOfType<SplitViewState>() != null;
|
||||
static bool isSecondaryVisible(BuildContext context) =>
|
||||
hasSplitViewState(context) && SplitView.of(context).isSecondaryVisible;
|
||||
|
||||
static void pushSplitView(BuildContext context, Widget view, {bool overrideToSingleSubScreen = false}) {
|
||||
if(isSecondaryVisible(context)) {
|
||||
static void pushSplitView(
|
||||
BuildContext context,
|
||||
Widget view, {
|
||||
bool overrideToSingleSubScreen = false,
|
||||
}) {
|
||||
if (isSecondaryVisible(context)) {
|
||||
var splitView = SplitView.of(context);
|
||||
overrideToSingleSubScreen ? splitView.setSecondary(view) : splitView.push(view);
|
||||
overrideToSingleSubScreen
|
||||
? splitView.setSecondary(view)
|
||||
: splitView.push(view);
|
||||
} else {
|
||||
pushScreen(context, screen: view, withNavBar: false);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ class AnswerReference extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
final GetChatResponseObject referenceMessage;
|
||||
final String? selfId;
|
||||
const AnswerReference({required this.context, required this.referenceMessage, required this.selfId, super.key});
|
||||
const AnswerReference({
|
||||
required this.context,
|
||||
required this.referenceMessage,
|
||||
required this.selfId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -16,15 +21,25 @@ class AnswerReference extends StatelessWidget {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: referenceMessage.actorId == selfId
|
||||
? style.getSelfStyle(false).color!.withGreen(200).withValues(alpha: 0.2)
|
||||
: style.getRemoteStyle(false).color!.withWhite(200).withValues(alpha: 0.2),
|
||||
? style
|
||||
.getSelfStyle(false)
|
||||
.color!
|
||||
.withGreen(200)
|
||||
.withValues(alpha: 0.2)
|
||||
: style
|
||||
.getRemoteStyle(false)
|
||||
.color!
|
||||
.withWhite(200)
|
||||
.withValues(alpha: 0.2),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border(left: BorderSide(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: referenceMessage.actorId == selfId
|
||||
? style.getSelfStyle(false).color!.withGreen(200)
|
||||
: style.getRemoteStyle(false).color!.withWhite(200),
|
||||
width: 5
|
||||
)),
|
||||
width: 5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5).add(const EdgeInsets.only(left: 5)),
|
||||
@@ -43,7 +58,10 @@ class AnswerReference extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
RichObjectStringProcessor.parseToString(referenceMessage.message, referenceMessage.messageParameters),
|
||||
RichObjectStringProcessor.parseToString(
|
||||
referenceMessage.message,
|
||||
referenceMessage.messageParameters,
|
||||
),
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -3,12 +3,17 @@ import 'package:flutter/material.dart';
|
||||
enum BubbleNip { leftTop, rightBottom, none }
|
||||
|
||||
class BubbleEdges {
|
||||
const BubbleEdges.only({this.top = 0, this.bottom = 0, this.left = 0, this.right = 0});
|
||||
const BubbleEdges.only({
|
||||
this.top = 0,
|
||||
this.bottom = 0,
|
||||
this.left = 0,
|
||||
this.right = 0,
|
||||
});
|
||||
const BubbleEdges.all(double value)
|
||||
: top = value,
|
||||
bottom = value,
|
||||
left = value,
|
||||
right = value;
|
||||
: top = value,
|
||||
bottom = value,
|
||||
left = value,
|
||||
right = value;
|
||||
|
||||
final double top;
|
||||
final double bottom;
|
||||
@@ -53,9 +58,19 @@ class Bubble extends StatelessWidget {
|
||||
final flat = Radius.zero;
|
||||
switch (style.nip) {
|
||||
case BubbleNip.leftTop:
|
||||
return BorderRadius.only(topLeft: flat, topRight: r, bottomLeft: r, bottomRight: r);
|
||||
return BorderRadius.only(
|
||||
topLeft: flat,
|
||||
topRight: r,
|
||||
bottomLeft: r,
|
||||
bottomRight: r,
|
||||
);
|
||||
case BubbleNip.rightBottom:
|
||||
return BorderRadius.only(topLeft: r, topRight: r, bottomLeft: r, bottomRight: flat);
|
||||
return BorderRadius.only(
|
||||
topLeft: r,
|
||||
topRight: r,
|
||||
bottomLeft: r,
|
||||
bottomRight: flat,
|
||||
);
|
||||
case BubbleNip.none:
|
||||
return BorderRadius.all(r);
|
||||
}
|
||||
@@ -72,10 +87,19 @@ class Bubble extends StatelessWidget {
|
||||
color: style.color,
|
||||
borderRadius: radius,
|
||||
border: style.borderWidth > 0
|
||||
? Border.all(color: Theme.of(context).dividerColor, width: style.borderWidth)
|
||||
? Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: style.borderWidth,
|
||||
)
|
||||
: null,
|
||||
boxShadow: style.elevation > 0
|
||||
? [BoxShadow(color: Colors.black26, blurRadius: style.elevation * 2, offset: Offset(0, style.elevation))]
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: style.elevation * 2,
|
||||
offset: Offset(0, style.elevation),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
padding: style.padding.toEdgeInsets(),
|
||||
|
||||
@@ -40,13 +40,15 @@ class ChatBubble extends StatefulWidget {
|
||||
required this.refetch,
|
||||
this.isRead = false,
|
||||
this.selfId,
|
||||
super.key});
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatBubble> createState() => _ChatBubbleState();
|
||||
}
|
||||
|
||||
class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateMixin {
|
||||
class _ChatBubbleState extends State<ChatBubble>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late ChatMessage message;
|
||||
DownloadJob? _job;
|
||||
|
||||
@@ -109,7 +111,10 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
|
||||
final file = message.file;
|
||||
final filePath = file?.path;
|
||||
if (file == null || filePath == null) return;
|
||||
final job = await DownloadManager.instance.start(remotePath: filePath, name: file.name);
|
||||
final job = await DownloadManager.instance.start(
|
||||
remotePath: filePath,
|
||||
name: file.name,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (_job == job) return;
|
||||
_detachJob();
|
||||
@@ -129,19 +134,22 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
|
||||
|
||||
BubbleStyle _getStyle() {
|
||||
final styles = ChatBubbleStyles(context);
|
||||
if (widget.bubbleData.messageType != GetRoomResponseObjectMessageType.comment) {
|
||||
if (widget.bubbleData.messageType !=
|
||||
GetRoomResponseObjectMessageType.comment) {
|
||||
return styles.getSystemStyle();
|
||||
}
|
||||
return widget.isSender ? styles.getSelfStyle(false) : styles.getRemoteStyle(false);
|
||||
return widget.isSender
|
||||
? styles.getSelfStyle(false)
|
||||
: styles.getRemoteStyle(false);
|
||||
}
|
||||
|
||||
void _showOptionsDialog() => showChatMessageOptionsDialog(
|
||||
context,
|
||||
chatData: widget.chatData,
|
||||
bubbleData: widget.bubbleData,
|
||||
isSender: widget.isSender,
|
||||
onRefetch: widget.refetch,
|
||||
);
|
||||
context,
|
||||
chatData: widget.chatData,
|
||||
bubbleData: widget.bubbleData,
|
||||
isSender: widget.isSender,
|
||||
onRefetch: widget.refetch,
|
||||
);
|
||||
|
||||
void _onTap() {
|
||||
final obj = message.originalData?['object'];
|
||||
@@ -165,24 +173,40 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters);
|
||||
final showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment
|
||||
&& widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
|
||||
final showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system
|
||||
&& widget.bubbleData.messageType != GetRoomResponseObjectMessageType.deletedComment;
|
||||
message = ChatMessage(
|
||||
originalMessage: widget.bubbleData.message,
|
||||
originalData: widget.bubbleData.messageParameters,
|
||||
);
|
||||
final showActorDisplayName =
|
||||
widget.bubbleData.messageType ==
|
||||
GetRoomResponseObjectMessageType.comment &&
|
||||
widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
|
||||
final showBubbleTime =
|
||||
widget.bubbleData.messageType !=
|
||||
GetRoomResponseObjectMessageType.system &&
|
||||
widget.bubbleData.messageType !=
|
||||
GetRoomResponseObjectMessageType.deletedComment;
|
||||
|
||||
final parent = widget.bubbleData.parent;
|
||||
final actorText = Text(
|
||||
widget.bubbleData.actorDisplayName,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
|
||||
final timeText = Text(
|
||||
DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).formatHm(),
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
widget.bubbleData.timestamp * 1000,
|
||||
).formatHm(),
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize),
|
||||
style: TextStyle(
|
||||
color: widget.timeIconColor,
|
||||
fontSize: widget.timeIconSize,
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
@@ -206,7 +230,9 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
|
||||
final isAction = _position.dx.abs() > 50;
|
||||
setState(() => _position = Offset.zero);
|
||||
if (widget.bubbleData.isReplyable && isAction) {
|
||||
context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
|
||||
context.read<ChatBloc>().setReferenceMessageId(
|
||||
widget.bubbleData.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: _showOptionsDialog,
|
||||
@@ -281,67 +307,68 @@ class _BubbleContent extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.9,
|
||||
minWidth: showActorDisplayName
|
||||
? actorText.size.width
|
||||
: timeText.size.width + (isSender ? spacing + timeIconSize : 0) + 3,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.9,
|
||||
minWidth: showActorDisplayName
|
||||
? actorText.size.width
|
||||
: timeText.size.width + (isSender ? spacing + timeIconSize : 0) + 3,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (showActorDisplayName) Positioned(top: 0, left: 0, child: actorText),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: showBubbleTime ? 18 : 0,
|
||||
top: showActorDisplayName ? 18 : 0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (parent != null &&
|
||||
bubbleData.messageType ==
|
||||
GetRoomResponseObjectMessageType.comment) ...[
|
||||
AnswerReference(
|
||||
context: context,
|
||||
referenceMessage: parent!,
|
||||
selfId: selfId,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
],
|
||||
messageWidget,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (showActorDisplayName)
|
||||
Positioned(top: 0, left: 0, child: actorText),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: showBubbleTime ? 18 : 0,
|
||||
top: showActorDisplayName ? 18 : 0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (parent != null && bubbleData.messageType == GetRoomResponseObjectMessageType.comment) ...[
|
||||
AnswerReference(
|
||||
context: context,
|
||||
referenceMessage: parent!,
|
||||
selfId: selfId,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
],
|
||||
messageWidget,
|
||||
if (showBubbleTime)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
children: [
|
||||
timeText,
|
||||
if (isSender) ...[
|
||||
SizedBox(width: spacing),
|
||||
Icon(
|
||||
isRead ? Icons.done_all_outlined : Icons.done_outlined,
|
||||
size: timeIconSize,
|
||||
color: timeIconColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showBubbleTime)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
children: [
|
||||
timeText,
|
||||
if (isSender) ...[
|
||||
SizedBox(width: spacing),
|
||||
Icon(
|
||||
isRead ? Icons.done_all_outlined : Icons.done_outlined,
|
||||
size: timeIconSize,
|
||||
color: timeIconColor,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (downloadJob?.status.value is DownloadInProgress)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: LinearProgressIndicator(
|
||||
value: () {
|
||||
final s = downloadJob!.status.value as DownloadInProgress;
|
||||
return s.percent <= 0 ? null : s.percent / 100;
|
||||
}(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
if (downloadJob?.status.value is DownloadInProgress)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: LinearProgressIndicator(
|
||||
value: () {
|
||||
final s = downloadJob!.status.value as DownloadInProgress;
|
||||
return s.percent <= 0 ? null : s.percent / 100;
|
||||
}(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ void showChatBubblePollDialog(
|
||||
future: pollState,
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Column(mainAxisSize: MainAxisSize.min, children: [LoadingSpinner()]);
|
||||
return const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [LoadingSpinner()],
|
||||
);
|
||||
}
|
||||
final pollData = snapshot.data!.data;
|
||||
return SingleChildScrollView(
|
||||
child: PollOptionsList(
|
||||
pollData: pollData,
|
||||
chatToken: chatToken,
|
||||
),
|
||||
child: PollOptionsList(pollData: pollData, chatToken: chatToken),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -37,14 +37,20 @@ class ChatBubbleReactions extends StatelessWidget {
|
||||
alignment: isSender ? WrapAlignment.end : WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
children: reactions.entries.map<Widget>((e) {
|
||||
final hasSelfReacted = bubbleData.reactionsSelf?.contains(e.key) ?? false;
|
||||
final hasSelfReacted =
|
||||
bubbleData.reactionsSelf?.contains(e.key) ?? false;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 2.5, left: 2.5),
|
||||
child: ActionChip(
|
||||
label: Text('${e.key} ${e.value}'),
|
||||
visualDensity: const VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity),
|
||||
visualDensity: const VisualDensity(
|
||||
vertical: VisualDensity.minimumDensity,
|
||||
horizontal: VisualDensity.minimumDensity,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null,
|
||||
backgroundColor: hasSelfReacted
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
onPressed: () {
|
||||
runWithErrorDialog(context, () async {
|
||||
if (hasSelfReacted) {
|
||||
|
||||
@@ -29,11 +29,13 @@ void showChatMessageOptionsDialog(
|
||||
required void Function({bool renew}) onRefetch,
|
||||
}) {
|
||||
final parentContext = context;
|
||||
final canReact = bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
|
||||
final canDelete = isSender &&
|
||||
DateTime.fromMillisecondsSinceEpoch(bubbleData.timestamp * 1000)
|
||||
.add(const Duration(hours: 6))
|
||||
.isAfter(DateTime.now());
|
||||
final canReact =
|
||||
bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
|
||||
final canDelete =
|
||||
isSender &&
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
bubbleData.timestamp * 1000,
|
||||
).add(const Duration(hours: 6)).isAfter(DateTime.now());
|
||||
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
@@ -61,7 +63,11 @@ void showChatMessageOptionsDialog(
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
if (!parentContext.mounted) return;
|
||||
AppRoutes.openMessageReactions(parentContext, chatData.token, bubbleData.id);
|
||||
AppRoutes.openMessageReactions(
|
||||
parentContext,
|
||||
chatData.token,
|
||||
bubbleData.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (bubbleData.message != '{file}')
|
||||
@@ -73,7 +79,9 @@ void showChatMessageOptionsDialog(
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
if (!kReleaseMode && !isSender && chatData.type != GetRoomResponseObjectConversationType.oneToOne)
|
||||
if (!kReleaseMode &&
|
||||
!isSender &&
|
||||
chatData.type != GetRoomResponseObjectConversationType.oneToOne)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.sms_outlined),
|
||||
title: Text("Private Nachricht an '${bubbleData.actorDisplayName}'"),
|
||||
@@ -136,54 +144,57 @@ class _ReactionsRowState extends State<_ReactionsRow> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
final busy = _controller.busy;
|
||||
final err = _controller.error;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
final busy = _controller.busy;
|
||||
final err = _controller.error;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
..._commonReactions.map(
|
||||
(emoji) => TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40),
|
||||
),
|
||||
onPressed: busy ? null : () => _react(emoji),
|
||||
child: Text(emoji),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: busy ? null : () => _showEmojiPicker(context),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40),
|
||||
),
|
||||
icon: busy
|
||||
? const AppProgressIndicator.small()
|
||||
: const Icon(Icons.add_circle_outline_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (err != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
err,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
|
||||
..._commonReactions.map(
|
||||
(emoji) => TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40),
|
||||
),
|
||||
onPressed: busy ? null : () => _react(emoji),
|
||||
child: Text(emoji),
|
||||
),
|
||||
const Divider(),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: busy ? null : () => _showEmojiPicker(context),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40),
|
||||
),
|
||||
icon: busy
|
||||
? const AppProgressIndicator.small()
|
||||
: const Icon(Icons.add_circle_outline_outlined),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (err != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
err,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
void _showEmojiPicker(BuildContext rowContext) {
|
||||
showDialog(
|
||||
@@ -214,7 +225,9 @@ class _ReactionsRowState extends State<_ReactionsRow> {
|
||||
noRecents: const Text('Keine zuletzt verwendeten Emojis'),
|
||||
columns: 7,
|
||||
),
|
||||
bottomActionBarConfig: const emojis.BottomActionBarConfig(enabled: false),
|
||||
bottomActionBarConfig: const emojis.BottomActionBarConfig(
|
||||
enabled: false,
|
||||
),
|
||||
categoryViewConfig: emojis.CategoryViewConfig(
|
||||
backgroundColor: Theme.of(pickerCtx).hoverColor,
|
||||
iconColorSelected: Theme.of(pickerCtx).primaryColor,
|
||||
|
||||
@@ -39,13 +39,17 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
void share(String shareFolder, List<String> filePaths) {
|
||||
for (final element in filePaths) {
|
||||
final fileName = element.split(Platform.pathSeparator).last;
|
||||
FileSharingApi().share(FileSharingApiParams(
|
||||
shareType: 10,
|
||||
shareWith: widget.sendToToken,
|
||||
path: '$shareFolder/$fileName',
|
||||
)).then((_) {
|
||||
if (mounted) context.read<ChatBloc>().refresh();
|
||||
});
|
||||
FileSharingApi()
|
||||
.share(
|
||||
FileSharingApiParams(
|
||||
shareType: 10,
|
||||
shareWith: widget.sendToToken,
|
||||
path: '$shareFolder/$fileName',
|
||||
),
|
||||
)
|
||||
.then((_) {
|
||||
if (mounted) context.read<ChatBloc>().refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,19 +57,25 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
if (paths == null) return;
|
||||
|
||||
const shareFolder = 'MarianumMobile';
|
||||
unawaited(WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder'))));
|
||||
unawaited(
|
||||
WebdavApi.webdav.then(
|
||||
(webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')),
|
||||
),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
unawaited(pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
filePaths: paths,
|
||||
remotePath: shareFolder,
|
||||
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
|
||||
uniqueNames: true,
|
||||
unawaited(
|
||||
pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
filePaths: paths,
|
||||
remotePath: shareFolder,
|
||||
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
|
||||
uniqueNames: true,
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
void _setDraft(String text) {
|
||||
@@ -82,7 +92,9 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
if (messageId != null) {
|
||||
talkSettings.draftReplies[widget.sendToToken] = messageId;
|
||||
} else {
|
||||
talkSettings.draftReplies.removeWhere((key, _) => key == widget.sendToToken);
|
||||
talkSettings.draftReplies.removeWhere(
|
||||
(key, _) => key == widget.sendToToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +102,10 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
settings = context.read<SettingsCubit>();
|
||||
final draftReply = settings.val().talkSettings.draftReplies[widget.sendToToken];
|
||||
final draftReply = settings
|
||||
.val()
|
||||
.talkSettings
|
||||
.draftReplies[widget.sendToToken];
|
||||
if (draftReply != null) {
|
||||
context.read<ChatBloc>().setReferenceMessageId(draftReply);
|
||||
}
|
||||
@@ -121,16 +136,19 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
|
||||
_textBoxController.text =
|
||||
settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
|
||||
final chatBloc = context.watch<ChatBloc>();
|
||||
final chatState = chatBloc.state.data;
|
||||
|
||||
Widget replyBanner = const SizedBox.shrink();
|
||||
if (chatState != null && chatState.referenceMessageId != null && chatState.chatResponse != null) {
|
||||
if (chatState != null &&
|
||||
chatState.referenceMessageId != null &&
|
||||
chatState.chatResponse != null) {
|
||||
try {
|
||||
final referenceMessage = chatState.chatResponse!.sortByTimestamp().firstWhere(
|
||||
(e) => e.id == chatState.referenceMessageId,
|
||||
);
|
||||
final referenceMessage = chatState.chatResponse!
|
||||
.sortByTimestamp()
|
||||
.firstWhere((e) => e.id == chatState.referenceMessageId);
|
||||
replyBanner = Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -150,120 +168,150 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
),
|
||||
],
|
||||
);
|
||||
} catch (_) {/* reference no longer in current chat data */}
|
||||
} catch (_) {
|
||||
/* reference no longer in current chat data */
|
||||
}
|
||||
}
|
||||
|
||||
return Stack(children: <Widget>[
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 10, bottom: 3, top: 3, right: 10),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
replyBanner,
|
||||
if (_sendError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
_sendError!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
|
||||
),
|
||||
),
|
||||
Row(children: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.file_open),
|
||||
title: const Text('Aus Dateien auswählen'),
|
||||
onTap: () {
|
||||
FilePick.documentPick().then(mediaUpload);
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.image),
|
||||
title: const Text('Aus Galerie auswählen'),
|
||||
onTap: () {
|
||||
FilePick.multipleGalleryPick().then((value) {
|
||||
if (value != null) mediaUpload(value.map((e) => e.path).toList());
|
||||
});
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt_outlined),
|
||||
title: const Text('Foto aufnehmen'),
|
||||
onTap: () {
|
||||
FilePick.cameraPick().then((image) {
|
||||
if (image != null) mediaUpload([image.path]);
|
||||
});
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
elevation: 5,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
||||
child: Container(
|
||||
height: 30,
|
||||
width: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
bottom: 3,
|
||||
top: 3,
|
||||
right: 10,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
replyBanner,
|
||||
if (_sendError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
_sendError!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
child: const Icon(Icons.attach_file_outlined, color: Colors.white, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autocorrect: true,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
controller: _textBoxController,
|
||||
maxLines: 7,
|
||||
minLines: 1,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Nachricht schreiben...',
|
||||
border: InputBorder.none,
|
||||
Row(
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.file_open),
|
||||
title: const Text('Aus Dateien auswählen'),
|
||||
onTap: () {
|
||||
FilePick.documentPick().then(mediaUpload);
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.image),
|
||||
title: const Text('Aus Galerie auswählen'),
|
||||
onTap: () {
|
||||
FilePick.multipleGalleryPick().then((value) {
|
||||
if (value != null) {
|
||||
mediaUpload(
|
||||
value.map((e) => e.path).toList(),
|
||||
);
|
||||
}
|
||||
});
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt_outlined),
|
||||
title: const Text('Foto aufnehmen'),
|
||||
onTap: () {
|
||||
FilePick.cameraPick().then((image) {
|
||||
if (image != null) mediaUpload([image.path]);
|
||||
});
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
elevation: 5,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Container(
|
||||
height: 30,
|
||||
width: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.attach_file_outlined,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (text) {
|
||||
if (text.trim().toLowerCase() == 'marbot marbot marbot') {
|
||||
const newText = 'Roboter sind cool und so, aber Marbots sind besser!';
|
||||
_textBoxController.text = newText;
|
||||
text = newText;
|
||||
}
|
||||
_setDraft(text);
|
||||
},
|
||||
onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autocorrect: true,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
controller: _textBoxController,
|
||||
maxLines: 7,
|
||||
minLines: 1,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Nachricht schreiben...',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onChanged: (text) {
|
||||
if (text.trim().toLowerCase() ==
|
||||
'marbot marbot marbot') {
|
||||
const newText =
|
||||
'Roboter sind cool und so, aber Marbots sind besser!';
|
||||
_textBoxController.text = newText;
|
||||
text = newText;
|
||||
}
|
||||
_setDraft(text);
|
||||
},
|
||||
onTapOutside: (_) =>
|
||||
FocusBehaviour.textFieldTapOutside(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textBoxController,
|
||||
builder: (context, value, _) => AsyncFab(
|
||||
mini: true,
|
||||
heroTag: 'chatSend_${widget.sendToToken}',
|
||||
icon: Icons.send,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
controller: _sendController,
|
||||
onPressed: value.text.trim().isEmpty
|
||||
? null
|
||||
: () => _sendMessage(chatBloc),
|
||||
onError: (message) =>
|
||||
setState(() => _sendError = message),
|
||||
onSuccess: () => setState(() => _sendError = null),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textBoxController,
|
||||
builder: (context, value, _) => AsyncFab(
|
||||
mini: true,
|
||||
heroTag: 'chatSend_${widget.sendToToken}',
|
||||
icon: Icons.send,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
controller: _sendController,
|
||||
onPressed: value.text.trim().isEmpty ? null : () => _sendMessage(chatBloc),
|
||||
onError: (message) => setState(() => _sendError = message),
|
||||
onSuccess: () => setState(() => _sendError = null),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,12 @@ class ChatTile extends StatefulWidget {
|
||||
final bool disableContextActions;
|
||||
final bool hasDraft;
|
||||
|
||||
const ChatTile({super.key, required this.data, this.disableContextActions = false, this.hasDraft = false});
|
||||
const ChatTile({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.disableContextActions = false,
|
||||
this.hasDraft = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatTile> createState() => _ChatTileState();
|
||||
@@ -39,7 +44,11 @@ class _ChatTileState extends State<ChatTile> {
|
||||
super.initState();
|
||||
AccountData().waitForPopulation().then((_) {
|
||||
if (!mounted) return;
|
||||
setState(() => selfUsername = AccountData().isPopulated() ? AccountData().getUsername() : null);
|
||||
setState(
|
||||
() => selfUsername = AccountData().isPopulated()
|
||||
? AccountData().getUsername()
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +58,9 @@ class _ChatTileState extends State<ChatTile> {
|
||||
await SetReadMarker(
|
||||
widget.data.token,
|
||||
true,
|
||||
setReadMarkerParams: SetReadMarkerParams(lastReadMessage: widget.data.lastMessage.id),
|
||||
setReadMarkerParams: SetReadMarkerParams(
|
||||
lastReadMessage: widget.data.lastMessage.id,
|
||||
),
|
||||
).run();
|
||||
if (!mounted) return;
|
||||
_refreshList();
|
||||
@@ -58,12 +69,18 @@ class _ChatTileState extends State<ChatTile> {
|
||||
@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);
|
||||
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)
|
||||
tileColor:
|
||||
chatBloc.state.data?.currentToken == widget.data.token &&
|
||||
TalkNavigator.isSecondaryVisible(context)
|
||||
? Theme.of(context).primaryColor.withAlpha(100)
|
||||
: null,
|
||||
leading: Stack(
|
||||
@@ -80,16 +97,25 @@ class _ChatTileState extends State<ChatTile> {
|
||||
color: Theme.of(context).primaryColor.withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(90.0),
|
||||
),
|
||||
child: const Icon(Icons.star, color: Colors.amberAccent, size: 15),
|
||||
child: const Icon(
|
||||
Icons.star,
|
||||
color: Colors.amberAccent,
|
||||
size: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(child: Text(widget.data.displayName, overflow: TextOverflow.ellipsis)),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.data.displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (widget.hasDraft) ...[
|
||||
const SizedBox(width: 5),
|
||||
const Icon(Icons.edit_outlined, size: 15),
|
||||
@@ -119,8 +145,16 @@ class _ChatTileState extends State<ChatTile> {
|
||||
onTap: () {
|
||||
if (selfUsername == null) return;
|
||||
unawaited(_setCurrentAsRead());
|
||||
final view = ChatView(room: widget.data, selfId: selfUsername!, avatar: circleAvatar);
|
||||
TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
|
||||
final view = ChatView(
|
||||
room: widget.data,
|
||||
selfId: selfUsername!,
|
||||
avatar: circleAvatar,
|
||||
);
|
||||
TalkNavigator.pushSplitView(
|
||||
context,
|
||||
view,
|
||||
overrideToSingleSubScreen: true,
|
||||
);
|
||||
context.read<ChatBloc>().setToken(widget.data.token);
|
||||
},
|
||||
onLongPress: () {
|
||||
@@ -168,7 +202,8 @@ class _ChatTileState extends State<ChatTile> {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
ConfirmDialog(
|
||||
title: 'Chat verlassen',
|
||||
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
|
||||
content:
|
||||
'Du benötigst ggf. eine Einladung um erneut beizutreten.',
|
||||
confirmButton: 'Verlassen',
|
||||
onConfirmAsync: () async {
|
||||
await LeaveRoom(widget.data.token).run();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
@@ -8,7 +7,11 @@ import '../../../../utils/url_opener.dart';
|
||||
class PollOptionsList extends StatefulWidget {
|
||||
final GetPollStateResponseObject pollData;
|
||||
final String chatToken;
|
||||
const PollOptionsList({super.key, required this.pollData, required this.chatToken});
|
||||
const PollOptionsList({
|
||||
super.key,
|
||||
required this.pollData,
|
||||
required this.chatToken,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PollOptionsList> createState() => _PollOptionsListState();
|
||||
@@ -23,44 +26,48 @@ class _PollOptionsListState extends State<PollOptionsList> {
|
||||
var votedSelf = widget.pollData.votedSelf.contains(optionId);
|
||||
var portionsVisible = widget.pollData.votes is Map<String, dynamic>;
|
||||
var votes = portionsVisible
|
||||
? (widget.pollData.votes['option-$optionId'] as num?) ?? 0
|
||||
: 0;
|
||||
? (widget.pollData.votes['option-$optionId'] as num?) ?? 0
|
||||
: 0;
|
||||
var numVoters = widget.pollData.numVoters ?? 0;
|
||||
final portion = numVoters == 0 ? 0.0 : (votes / numVoters);
|
||||
|
||||
return ListTile(
|
||||
isThreeLine: portionsVisible,
|
||||
dense: true,
|
||||
title: Text(
|
||||
option,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
title: Text(option, style: Theme.of(context).textTheme.bodyLarge),
|
||||
leading: Icon(
|
||||
votedSelf ? Icons.check_circle_outlined : Icons.circle_outlined,
|
||||
color: votedSelf
|
||||
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.6)
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
subtitle: portionsVisible ? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(value: portion.clamp(0.0, 1.0)),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 10),
|
||||
child: Text('${(portion * 100).round()}%'),
|
||||
),
|
||||
],
|
||||
) : null,
|
||||
subtitle: portionsVisible
|
||||
? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
value: portion.clamp(0.0, 1.0),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 10),
|
||||
child: Text('${(portion * 100).round()}%'),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
ListTile(
|
||||
title: Linkify(
|
||||
text: 'Wenn du abstimmen möchtest, verwende die Webversion unter https://cloud.marianum-fulda.de/call/${widget.chatToken}',
|
||||
text:
|
||||
'Wenn du abstimmen möchtest, verwende die Webversion unter https://cloud.marianum-fulda.de/call/${widget.chatToken}',
|
||||
onOpen: UrlOpener.onOpen,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,21 +7,25 @@ class SplitViewPlaceholder extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
invertColors: !AppTheme.isDarkMode(context),
|
||||
),
|
||||
child: Image.asset('assets/logo/icon.png', height: 200),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
const Text('Marianum Fulda\nTalk', textAlign: TextAlign.center, style: TextStyle(fontSize: 30)),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MediaQuery(
|
||||
data: MediaQuery.of(
|
||||
context,
|
||||
).copyWith(invertColors: !AppTheme.isDarkMode(context)),
|
||||
child: Image.asset('assets/logo/icon.png', height: 200),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
const Text(
|
||||
'Marianum Fulda\nTalk',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 30),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user