Merge pull request 'develop-replyToMessages' (#67) from develop-replyToMessages into develop

Reviewed-on: #67
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
This commit is contained in:
Lars Neuhaus 2024-05-12 12:36:14 +00:00
commit 430f2fe603
16 changed files with 402 additions and 190 deletions

View File

@ -38,6 +38,7 @@ class GetChatResponseObject {
Map<String, int>? reactions; Map<String, int>? reactions;
List<String>? reactionsSelf; List<String>? reactionsSelf;
@JsonKey(fromJson: _fromJson) Map<String, RichObjectString>? messageParameters; @JsonKey(fromJson: _fromJson) Map<String, RichObjectString>? messageParameters;
GetChatResponseObject? parent;
GetChatResponseObject( GetChatResponseObject(
this.id, this.id,
@ -53,7 +54,8 @@ class GetChatResponseObject {
this.message, this.message,
this.messageParameters, this.messageParameters,
this.reactions, this.reactions,
this.reactionsSelf this.reactionsSelf,
this.parent,
); );
factory GetChatResponseObject.fromJson(Map<String, dynamic> json) => _$GetChatResponseObjectFromJson(json); factory GetChatResponseObject.fromJson(Map<String, dynamic> json) => _$GetChatResponseObjectFromJson(json);
@ -78,7 +80,8 @@ class GetChatResponseObject {
text, text,
null, null,
null, null,
null null,
null,
); );
} }

View File

@ -52,6 +52,10 @@ GetChatResponseObject _$GetChatResponseObjectFromJson(
(json['reactionsSelf'] as List<dynamic>?) (json['reactionsSelf'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList(), .toList(),
json['parent'] == null
? null
: GetChatResponseObject.fromJson(
json['parent'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$GetChatResponseObjectToJson( Map<String, dynamic> _$GetChatResponseObjectToJson(
@ -74,6 +78,7 @@ Map<String, dynamic> _$GetChatResponseObjectToJson(
'reactionsSelf': instance.reactionsSelf, 'reactionsSelf': instance.reactionsSelf,
'messageParameters': 'messageParameters':
instance.messageParameters?.map((k, e) => MapEntry(k, e.toJson())), instance.messageParameters?.map((k, e) => MapEntry(k, e.toJson())),
'parent': instance.parent?.toJson(),
}; };
const _$GetRoomResponseObjectMessageActorTypeEnumMap = { const _$GetRoomResponseObjectMessageActorTypeEnumMap = {

View File

@ -7,7 +7,7 @@ part 'sendMessageParams.g.dart';
@JsonSerializable(explicitToJson: true, includeIfNull: false) @JsonSerializable(explicitToJson: true, includeIfNull: false)
class SendMessageParams extends ApiParams { class SendMessageParams extends ApiParams {
String message; String message;
int? replyTo; String? replyTo;
SendMessageParams(this.message, {this.replyTo}); SendMessageParams(this.message, {this.replyTo});

View File

@ -9,7 +9,7 @@ part of 'sendMessageParams.dart';
SendMessageParams _$SendMessageParamsFromJson(Map<String, dynamic> json) => SendMessageParams _$SendMessageParamsFromJson(Map<String, dynamic> json) =>
SendMessageParams( SendMessageParams(
json['message'] as String, json['message'] as String,
replyTo: json['replyTo'] as int?, replyTo: json['replyTo'] as String?,
); );
Map<String, dynamic> _$SendMessageParamsToJson(SendMessageParams instance) { Map<String, dynamic> _$SendMessageParamsToJson(SendMessageParams instance) {

View File

@ -11,6 +11,7 @@ import 'package:badges/badges.dart' as badges;
import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
import 'api/mhsl/server/userIndex/update/updateUserindex.dart'; import 'api/mhsl/server/userIndex/update/updateUserindex.dart';
import 'main.dart';
import 'model/breakers/Breaker.dart'; import 'model/breakers/Breaker.dart';
import 'model/breakers/BreakerProps.dart'; import 'model/breakers/BreakerProps.dart';
import 'model/chatList/chatListProps.dart'; import 'model/chatList/chatListProps.dart';
@ -28,13 +29,12 @@ import 'view/pages/timetable/timetable.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
const App({super.key}); const App({super.key});
static PersistentTabController bottomNavigator = PersistentTabController(initialIndex: 0);
@override @override
State<App> createState() => _AppState(); State<App> createState() => _AppState();
} }
class _AppState extends State<App> with WidgetsBindingObserver { class _AppState extends State<App> with WidgetsBindingObserver {
late Timer refetchChats; late Timer refetchChats;
late Timer updateTimings; late Timer updateTimings;
@ -58,6 +58,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
@override @override
void initState() { void initState() {
Main.bottomNavigator = PersistentTabController(initialIndex: 0);
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<BreakerProps>(context, listen: false).run(); Provider.of<BreakerProps>(context, listen: false).run();
@ -95,7 +96,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) => PersistentTabView( Widget build(BuildContext context) => PersistentTabView(
controller: App.bottomNavigator, controller: Main.bottomNavigator,
navBarOverlap: const NavBarOverlap.none(), navBarOverlap: const NavBarOverlap.none(),
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,

View File

@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:loader_overlay/loader_overlay.dart'; import 'package:loader_overlay/loader_overlay.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -74,6 +75,8 @@ Future<void> main() async {
class Main extends StatefulWidget { class Main extends StatefulWidget {
const Main({super.key}); const Main({super.key});
static PersistentTabController bottomNavigator = PersistentTabController(initialIndex: 0);
@override @override
State<Main> createState() => _MainState(); State<Main> createState() => _MainState();

View File

@ -1,15 +1,23 @@
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import '../../api/apiResponse.dart'; import '../../api/apiResponse.dart';
import '../../api/marianumcloud/talk/chat/getChatCache.dart'; import '../../api/marianumcloud/talk/chat/getChatCache.dart';
import '../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../storage/base/settingsProvider.dart';
import '../dataHolder.dart'; import '../dataHolder.dart';
class ChatProps extends DataHolder { class ChatProps extends DataHolder {
String _queryToken = ''; String _queryToken = '';
DateTime _lastTokenSet = DateTime.now(); DateTime _lastTokenSet = DateTime.now();
int? _referenceMessageId;
GetChatResponse? _getChatResponse; GetChatResponse? _getChatResponse;
GetChatResponse get getChatResponse => _getChatResponse!; GetChatResponse get getChatResponse => _getChatResponse!;
int? get getReferenceMessageId => _referenceMessageId;
set unsafeInternalSetReferenceMessageId(int? reference) => _referenceMessageId = reference;
@override @override
List<ApiResponse?> properties() => [_getChatResponse]; List<ApiResponse?> properties() => [_getChatResponse];
@ -30,6 +38,20 @@ class ChatProps extends DataHolder {
); );
} }
void setReferenceMessageId(int? messageId, BuildContext context, String sendToToken) {
Future.microtask(() {
_referenceMessageId = messageId;
notifyListeners();
var settings = Provider.of<SettingsProvider>(context, listen: false);
if(messageId != null) {
settings.val(write: true).talkSettings.draftReplies[sendToToken] = messageId;
} else {
settings.val(write: true).talkSettings.draftReplies.removeWhere((key, value) => key == sendToToken);
}
});
}
void setQueryToken(String token) { void setQueryToken(String token) {
_queryToken = token; _queryToken = token;
_getChatResponse = null; _getChatResponse = null;

View File

@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../app.dart'; import '../main.dart';
import '../model/chatList/chatListProps.dart'; import '../model/chatList/chatListProps.dart';
import '../model/chatList/chatProps.dart'; import '../model/chatList/chatProps.dart';
@ -18,6 +18,6 @@ class NotificationTasks {
} }
static void navigateToTalk() { static void navigateToTalk() {
App.bottomNavigator.jumpToTab(1); Main.bottomNavigator.jumpToTab(1);
} }
} }

View File

@ -7,8 +7,9 @@ class TalkSettings {
bool sortFavoritesToTop; bool sortFavoritesToTop;
bool sortUnreadToTop; bool sortUnreadToTop;
Map<String, String> drafts; Map<String, String> drafts;
Map<String, int> draftReplies;
TalkSettings({required this.sortFavoritesToTop, required this.sortUnreadToTop, required this.drafts}); TalkSettings({required this.sortFavoritesToTop, required this.sortUnreadToTop, required this.drafts, required this.draftReplies});
factory TalkSettings.fromJson(Map<String, dynamic> json) => _$TalkSettingsFromJson(json); factory TalkSettings.fromJson(Map<String, dynamic> json) => _$TalkSettingsFromJson(json);
Map<String, dynamic> toJson() => _$TalkSettingsToJson(this); Map<String, dynamic> toJson() => _$TalkSettingsToJson(this);

View File

@ -10,6 +10,7 @@ TalkSettings _$TalkSettingsFromJson(Map<String, dynamic> json) => TalkSettings(
sortFavoritesToTop: json['sortFavoritesToTop'] as bool, sortFavoritesToTop: json['sortFavoritesToTop'] as bool,
sortUnreadToTop: json['sortUnreadToTop'] as bool, sortUnreadToTop: json['sortUnreadToTop'] as bool,
drafts: Map<String, String>.from(json['drafts'] as Map), drafts: Map<String, String>.from(json['drafts'] as Map),
draftReplies: Map<String, int>.from(json['draftReplies'] as Map),
); );
Map<String, dynamic> _$TalkSettingsToJson(TalkSettings instance) => Map<String, dynamic> _$TalkSettingsToJson(TalkSettings instance) =>
@ -17,4 +18,5 @@ Map<String, dynamic> _$TalkSettingsToJson(TalkSettings instance) =>
'sortFavoritesToTop': instance.sortFavoritesToTop, 'sortFavoritesToTop': instance.sortFavoritesToTop,
'sortUnreadToTop': instance.sortUnreadToTop, 'sortUnreadToTop': instance.sortUnreadToTop,
'drafts': instance.drafts, 'drafts': instance.drafts,
'draftReplies': instance.draftReplies,
}; };

View File

@ -71,6 +71,7 @@ class _ChatViewState extends State<ChatView> {
chatData: widget.room, chatData: widget.room,
refetch: _query, refetch: _query,
isRead: element.id <= commonRead, isRead: element.id <= commonRead,
selfId: widget.selfId,
) )
); );
}); });
@ -129,8 +130,8 @@ class _ChatViewState extends State<ChatView> {
Container( Container(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: TalkNavigator.isSecondaryVisible(context) child: TalkNavigator.isSecondaryVisible(context)
? ChatTextfield(widget.room.token) ? ChatTextfield(widget.room.token, selfId: widget.selfId)
: SafeArea(child: ChatTextfield(widget.room.token) : SafeArea(child: ChatTextfield(widget.room.token, selfId: widget.selfId)
), ),
) )
], ],

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import 'chatBubbleStyles.dart';
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});
@override
Widget build(BuildContext context) {
var style = ChatBubbleStyles(context);
return DecoratedBox(
decoration: BoxDecoration(
color: referenceMessage.actorId == selfId
? style.getSelfStyle(false).color!.withGreen(200).withOpacity(0.2)
: style.getRemoteStyle(false).color!.withWhite(200).withOpacity(0.2),
borderRadius: const BorderRadius.all(Radius.circular(5)),
border: Border(left: BorderSide(
color: referenceMessage.actorId == selfId
? style.getSelfStyle(false).color!.withGreen(200)
: style.getRemoteStyle(false).color!.withWhite(200),
width: 5
)),
),
child: Padding(
padding: const EdgeInsets.all(5).add(const EdgeInsets.only(left: 5)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
referenceMessage.actorDisplayName,
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: referenceMessage.actorId == selfId
? style.getSelfStyle(false).color!.withGreen(200)
: style.getRemoteStyle(false).color!.withWhite(200),
fontSize: 12,
),
),
Text(
RichObjectStringProcessor.parseToString(referenceMessage.message, referenceMessage.messageParameters),
maxLines: 2,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: Theme.of(context).colorScheme.onSurface,
fontSize: 12,
),
),
],
),
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:better_open_file/better_open_file.dart';
import 'package:bubble/bubble.dart'; import 'package:bubble/bubble.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
import 'package:flowder/flowder.dart'; import 'package:flowder/flowder.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -17,9 +18,10 @@ import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'; import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../model/chatList/chatProps.dart'; import '../../../../model/chatList/chatProps.dart';
import '../../../../theming/appTheme.dart';
import '../../../../widget/debug/debugTile.dart'; import '../../../../widget/debug/debugTile.dart';
import '../../files/fileElement.dart'; import '../../files/fileElement.dart';
import 'answerReference.dart';
import 'chatBubbleStyles.dart';
import 'chatMessage.dart'; import 'chatMessage.dart';
import '../messageReactions.dart'; import '../messageReactions.dart';
@ -29,6 +31,7 @@ class ChatBubble extends StatefulWidget {
final GetChatResponseObject bubbleData; final GetChatResponseObject bubbleData;
final GetRoomResponseObject chatData; final GetRoomResponseObject chatData;
final bool isRead; final bool isRead;
final String? selfId;
final double spacing = 3; final double spacing = 3;
final double timeIconSize = 11; final double timeIconSize = 11;
@ -43,59 +46,31 @@ class ChatBubble extends StatefulWidget {
required this.chatData, required this.chatData,
required this.refetch, required this.refetch,
this.isRead = false, this.isRead = false,
this.selfId,
super.key}); super.key});
@override @override
State<ChatBubble> createState() => _ChatBubbleState(); State<ChatBubble> createState() => _ChatBubbleState();
} }
class _ChatBubbleState extends State<ChatBubble> { class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateMixin {
BubbleStyle getSystemStyle() => BubbleStyle(
color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white,
borderWidth: 1,
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;
return BubbleStyle(
nip: BubbleNip.leftTop,
color: seamless ? Colors.transparent : color,
borderWidth: seamless ? 0 : 1,
elevation: seamless ? 0 : 1,
margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50),
alignment: Alignment.topLeft,
);
}
BubbleStyle getSelfStyle(bool seamless) {
var color = AppTheme.isDarkMode(context) ? const Color(0xff005c4b) : const Color(0xffd3d3d3);
return BubbleStyle(
nip: BubbleNip.rightBottom,
color: seamless ? Colors.transparent : color,
borderWidth: seamless ? 0 : 1,
elevation: seamless ? 0 : 1,
margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50),
alignment: Alignment.topRight,
);
}
late ChatMessage message; late ChatMessage message;
double downloadProgress = 0; double downloadProgress = 0;
Future<DownloaderCore>? downloadCore; Future<DownloaderCore>? downloadCore;
late Offset _position = const Offset(0, 0);
late Offset _dragStartPosition = Offset.zero;
BubbleStyle getStyle() { BubbleStyle getStyle() {
var styles = ChatBubbleStyles(context);
if(widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) { if(widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) {
if(widget.isSender) { if(widget.isSender) {
return getSelfStyle(false); return styles.getSelfStyle(false);
} else { } else {
return getRemoteStyle(false); return styles.getRemoteStyle(false);
} }
} else { } else {
return getSystemStyle(); return styles.getSystemStyle();
} }
} }
@ -207,6 +182,17 @@ class _ChatBubbleState extends State<ChatBubble> {
], ],
), ),
), ),
Visibility(
visible: widget.bubbleData.isReplyable,
child: ListTile(
leading: const Icon(Icons.reply_outlined),
title: const Text('Antworten'),
onTap: () => {
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(widget.bubbleData.id, context, widget.chatData.token),
Navigator.of(context).pop(),
},
),
),
Visibility( Visibility(
visible: canReact, visible: canReact,
child: ListTile( child: ListTile(
@ -265,8 +251,10 @@ class _ChatBubbleState extends State<ChatBubble> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters); message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters);
var showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne; var showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
var showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system; var showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system
&& widget.bubbleData.messageType != GetRoomResponseObjectMessageType.deletedComment;
var parent = widget.bubbleData.parent;
var actorText = Text( var actorText = Text(
widget.bubbleData.actorDisplayName, widget.bubbleData.actorDisplayName,
textAlign: TextAlign.start, textAlign: TextAlign.start,
@ -288,6 +276,24 @@ class _ChatBubbleState extends State<ChatBubble> {
children: [ children: [
GestureDetector( GestureDetector(
onHorizontalDragStart: (details) {
_dragStartPosition = _position;
},
onHorizontalDragUpdate: (details) {
if(!widget.bubbleData.isReplyable) return;
var dx = details.delta.dx - _dragStartPosition.dx;
setState(() {
_position = (_position.dx + dx).abs() > 30 ? Offset(_position.dx, 0) : Offset(_position.dx + dx, 0);
});
},
onHorizontalDragEnd: (DragEndDetails details) {
setState(() {
_position = const Offset(0, 0);
});
if(widget.bubbleData.isReplyable) {
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(widget.bubbleData.id, context, widget.chatData.token);
}
},
onLongPress: showOptionsDialog, onLongPress: showOptionsDialog,
onDoubleTap: showOptionsDialog, onDoubleTap: showOptionsDialog,
onTap: () { onTap: () {
@ -336,55 +342,76 @@ class _ChatBubbleState extends State<ChatBubble> {
} }
}); });
}, },
child: Bubble( child: Transform.translate(
style: getStyle(), offset: _position,
child: Container( child: Bubble(
constraints: BoxConstraints( style: getStyle(),
maxWidth: MediaQuery.of(context).size.width * 0.9, child: Column(
minWidth: showActorDisplayName
? actorText.size.width
: timeText.size.width + (widget.isSender ? widget.spacing + widget.timeIconSize : 0) + 3,
),
child: Stack(
children: [ children: [
Padding( Container(
padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0), constraints: BoxConstraints(
child: message.getWidget() maxWidth: MediaQuery.of(context).size.width * 0.9,
), minWidth: showActorDisplayName
Visibility( ? actorText.size.width
visible: showActorDisplayName, : timeText.size.width + (widget.isSender ? widget.spacing + widget.timeIconSize : 0) + 3,
child: Positioned(
top: 0,
left: 0,
child: actorText
), ),
), child: Stack(
Visibility( children: [
visible: showBubbleTime, Visibility(
child: Positioned( visible: showActorDisplayName,
bottom: 0, child: Positioned(
right: 0, top: 0,
child: Row( left: 0,
children: [ child: actorText
timeText, ),
if(widget.isSender) ...[ ),
SizedBox(width: widget.spacing), Padding(
if(widget.isRead) padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0),
Icon(Icons.done_all_outlined, size: widget.timeIconSize, color: widget.timeIconColor) child: Column(
else crossAxisAlignment: CrossAxisAlignment.start,
Icon(Icons.done_outlined, size: widget.timeIconSize, color: widget.timeIconColor) children: [
] if(parent != null && widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) ...[
], AnswerReference(
) context: context,
), referenceMessage: parent,
), selfId: widget.selfId,
Visibility( ),
visible: downloadProgress > 0, const SizedBox(height: 5),
child: Positioned( ],
bottom: 0, message.getWidget(),
right: 0, ],
left: 0, ),
child: LinearProgressIndicator(value: downloadProgress/100), ),
Visibility(
visible: showBubbleTime,
child: Positioned(
bottom: 0,
right: 0,
child: Row(
children: [
timeText,
if(widget.isSender) ...[
SizedBox(width: widget.spacing),
Icon(
widget.isRead ? Icons.done_all_outlined: Icons.done_outlined,
size: widget.timeIconSize,
color: widget.timeIconColor
)
]
],
)
),
),
Visibility(
visible: downloadProgress > 0,
child: Positioned(
bottom: 0,
right: 0,
left: 0,
child: LinearProgressIndicator(value: downloadProgress/100),
),
),
],
), ),
), ),
], ],
@ -417,9 +444,7 @@ class _ChatBubbleState extends State<ChatBubble> {
DeleteReactMessage( DeleteReactMessage(
chatToken: widget.chatData.token, chatToken: widget.chatData.token,
messageId: widget.bubbleData.id, messageId: widget.bubbleData.id,
params: DeleteReactMessageParams( params: DeleteReactMessageParams(e.key),
e.key
),
).run().then((value) => widget.refetch(renew: true)); ).run().then((value) => widget.refetch(renew: true));
} else { } else {
@ -427,11 +452,8 @@ class _ChatBubbleState extends State<ChatBubble> {
ReactMessage( ReactMessage(
chatToken: widget.chatData.token, chatToken: widget.chatData.token,
messageId: widget.bubbleData.id, messageId: widget.bubbleData.id,
params: ReactMessageParams( params: ReactMessageParams(e.key)
e.key
)
).run().then((value) => widget.refetch(renew: true)); ).run().then((value) => widget.refetch(renew: true));
} }
}, },
), ),

View File

@ -0,0 +1,54 @@
import 'package:bubble/bubble.dart';
import 'package:flutter/material.dart';
import '../../../../theming/appTheme.dart';
extension ColorExtensions on Color {
Color invert() {
final r = 255 - red;
final g = 255 - green;
final b = 255 - blue;
return Color.fromARGB((opacity * 255).round(), r, g, b);
}
Color withWhite(int whiteValue) => Color.fromARGB(alpha, whiteValue, whiteValue, whiteValue);
}
class ChatBubbleStyles {
final BuildContext context;
ChatBubbleStyles(this.context);
BubbleStyle getSystemStyle() => BubbleStyle(
color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white,
borderWidth: 1,
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;
return BubbleStyle(
nip: BubbleNip.leftTop,
color: seamless ? Colors.transparent : color,
borderWidth: seamless ? 0 : 1,
elevation: seamless ? 0 : 1,
margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50),
alignment: Alignment.topLeft,
);
}
BubbleStyle getSelfStyle(bool seamless) {
var color = AppTheme.isDarkMode(context) ? const Color(0xff005c4b) : const Color(0xffd3d3d3);
return BubbleStyle(
nip: BubbleNip.rightBottom,
color: seamless ? Colors.transparent : color,
borderWidth: seamless ? 0 : 1,
elevation: seamless ? 0 : 1,
margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50),
alignment: Alignment.topRight,
);
}
}

View File

@ -15,10 +15,12 @@ import '../../../../storage/base/settingsProvider.dart';
import '../../../../widget/filePick.dart'; import '../../../../widget/filePick.dart';
import '../../../../widget/focusBehaviour.dart'; import '../../../../widget/focusBehaviour.dart';
import '../../files/filesUploadDialog.dart'; import '../../files/filesUploadDialog.dart';
import 'answerReference.dart';
class ChatTextfield extends StatefulWidget { class ChatTextfield extends StatefulWidget {
final String sendToToken; final String sendToToken;
const ChatTextfield(this.sendToToken, {super.key}); final String? selfId;
const ChatTextfield(this.sendToToken, {this.selfId, super.key});
@override @override
State<ChatTextfield> createState() => _ChatTextfieldState(); State<ChatTextfield> createState() => _ChatTextfieldState();
@ -78,6 +80,8 @@ class _ChatTextfieldState extends State<ChatTextfield> {
void initState() { void initState() {
super.initState(); super.initState();
settings = Provider.of<SettingsProvider>(context, listen: false); settings = Provider.of<SettingsProvider>(context, listen: false);
Provider.of<ChatProps>(context, listen: false).unsafeInternalSetReferenceMessageId =
settings.val().talkSettings.draftReplies[widget.sendToToken];
} }
@override @override
@ -91,102 +95,136 @@ class _ChatTextfieldState extends State<ChatTextfield> {
child: Container( child: Container(
padding: const EdgeInsets.only(left: 10, bottom: 3, top: 3, right: 10), padding: const EdgeInsets.only(left: 10, bottom: 3, top: 3, right: 10),
width: double.infinity, width: double.infinity,
child: Row( child: Column(
children: <Widget>[ children: [
GestureDetector( Consumer<ChatProps>(
onTap: (){ builder: (context, data, child) {
showDialog(context: context, builder: (context) => SimpleDialog( if(data.getReferenceMessageId != null) {
var referenceMessage = data.getChatResponse.sortByTimestamp().where((element) => element.id == data.getReferenceMessageId).last;
return Row(
children: [ children: [
ListTile( Expanded(
leading: const Icon(Icons.file_open), child: AnswerReference(
title: const Text('Aus Dateien auswählen'), context: context,
onTap: () { referenceMessage: referenceMessage,
FilePick.documentPick().then(mediaUpload); selfId: widget.selfId,
Navigator.of(context).pop();
},
),
Visibility(
visible: !Platform.isIOS,
child: ListTile(
leading: const Icon(Icons.image),
title: const Text('Aus Gallerie auswählen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if(value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(context).pop();
},
), ),
), ),
IconButton(
onPressed: () => data.setReferenceMessageId(null, context, widget.sendToToken),
icon: const Icon(Icons.close_outlined),
padding: const EdgeInsets.only(left: 0),
),
], ],
)); );
} else {
return const SizedBox.shrink();
}
}, },
child: Material( ),
elevation: 5, Row(
shape: RoundedRectangleBorder( children: <Widget>[
borderRadius: BorderRadius.circular(30), GestureDetector(
onTap: (){
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.file_open),
title: const Text('Aus Dateien auswählen'),
onTap: () {
FilePick.documentPick().then(mediaUpload);
Navigator.of(context).pop();
},
),
Visibility(
visible: !Platform.isIOS,
child: ListTile(
leading: const Icon(Icons.image),
title: const Text('Aus Gallerie auswählen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if(value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(context).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, ),
),
)
), ),
child: Container( const SizedBox(width: 15),
height: 30, Expanded(
width: 30, child: TextField(
decoration: BoxDecoration( autocorrect: true,
color: Theme.of(context).primaryColor, textCapitalization: TextCapitalization.sentences,
borderRadius: BorderRadius.circular(30), controller: _textBoxController,
maxLines: 7,
minLines: 1,
decoration: const InputDecoration(
hintText: 'Nachricht schreiben...',
border: InputBorder.none,
),
onChanged: (String text) {
if(text.trim().toLowerCase() == 'marbot marbot marbot') {
var newText = 'Roboter sind cool und so, aber Marbots sind besser!';
_textBoxController.text = newText;
text = newText;
}
setDraft(text);
},
onTapOutside: (PointerDownEvent event) => FocusBehaviour.textFieldTapOutside(context),
), ),
child: const Icon(Icons.attach_file_outlined, color: Colors.white, size: 20, ),
), ),
) const SizedBox(width: 15),
), FloatingActionButton(
const SizedBox(width: 15), mini: true,
Expanded( onPressed: () {
child: TextField( if(_textBoxController.text.isEmpty) return;
autocorrect: true, if(isLoading) return;
textCapitalization: TextCapitalization.sentences,
controller: _textBoxController,
maxLines: 7,
minLines: 1,
decoration: const InputDecoration(
hintText: 'Nachricht schreiben...',
border: InputBorder.none,
),
onChanged: (String text) {
if(text.trim().toLowerCase() == 'marbot marbot marbot') {
var newText = 'Roboter sind cool und so, aber Marbots sind besser!';
_textBoxController.text = newText;
text = newText;
}
setDraft(text);
},
onTapOutside: (PointerDownEvent event) => FocusBehaviour.textFieldTapOutside(context),
),
),
const SizedBox(width: 15),
FloatingActionButton(
mini: true,
onPressed: (){
if(_textBoxController.text.isEmpty) return;
if(isLoading) return;
setState(() { setState(() {
isLoading = true; isLoading = true;
}); });
SendMessage(widget.sendToToken, SendMessageParams(_textBoxController.text)).run().then((value) { SendMessage(widget.sendToToken, SendMessageParams(
_query(); _textBoxController.text,
setState(() { replyTo: Provider.of<ChatProps>(context, listen: false).getReferenceMessageId.toString()
isLoading = false; )).run().then((value) {
}); _query();
_textBoxController.text = ''; setState(() {
setDraft(''); isLoading = false;
}); });
}, _textBoxController.text = '';
backgroundColor: Theme.of(context).primaryColor, setDraft('');
elevation: 5, Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(null, context, widget.sendToToken);
child: isLoading });
? Container(padding: const EdgeInsets.all(10), child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) },
: const Icon(Icons.send, color: Colors.white, size: 18), backgroundColor: Theme.of(context).primaryColor,
elevation: 5,
child: isLoading
? Container(padding: const EdgeInsets.all(10), child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Icon(Icons.send, color: Colors.white, size: 18),
),
],
), ),
], ],
), ),
), ),
), ),
], ],

View File

@ -29,6 +29,7 @@ class DefaultSettings {
sortFavoritesToTop: true, sortFavoritesToTop: true,
sortUnreadToTop: false, sortUnreadToTop: false,
drafts: {}, drafts: {},
draftReplies: {},
), ),
fileSettings: FileSettings( fileSettings: FileSettings(
sortFoldersToTop: true, sortFoldersToTop: true,