dart format

This commit is contained in:
2026-05-08 20:12:40 +02:00
parent 9e139b5704
commit 3b8da1d3d6
295 changed files with 6404 additions and 4161 deletions
+29 -9
View File
@@ -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(),
);
+98 -71
View File
@@ -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,
+46 -37
View File
@@ -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,
],
)
],
),
);
}
}
+23 -13
View File
@@ -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(),
],
),
),
],
)
),
);
}
}
+14 -12
View File
@@ -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);
}
+14 -5
View File
@@ -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) {
+13 -6
View File
@@ -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,
+33 -9
View File
@@ -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(),
+107 -80
View File
@@ -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,
+178 -130
View File
@@ -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),
),
),
]),
],
],
),
),
),
),
]);
],
);
}
}
+47 -12
View File
@@ -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),
),
],
),
),
);
}