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,9 +342,13 @@ class _ChatBubbleState extends State<ChatBubble> {
} }
}); });
}, },
child: Transform.translate(
offset: _position,
child: Bubble( child: Bubble(
style: getStyle(), style: getStyle(),
child: Container( child: Column(
children: [
Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.9, maxWidth: MediaQuery.of(context).size.width * 0.9,
minWidth: showActorDisplayName minWidth: showActorDisplayName
@ -347,10 +357,6 @@ class _ChatBubbleState extends State<ChatBubble> {
), ),
child: Stack( child: Stack(
children: [ children: [
Padding(
padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0),
child: message.getWidget()
),
Visibility( Visibility(
visible: showActorDisplayName, visible: showActorDisplayName,
child: Positioned( child: Positioned(
@ -359,6 +365,23 @@ class _ChatBubbleState extends State<ChatBubble> {
child: actorText child: actorText
), ),
), ),
Padding(
padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(parent != null && widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) ...[
AnswerReference(
context: context,
referenceMessage: parent,
selfId: widget.selfId,
),
const SizedBox(height: 5),
],
message.getWidget(),
],
),
),
Visibility( Visibility(
visible: showBubbleTime, visible: showBubbleTime,
child: Positioned( child: Positioned(
@ -369,10 +392,11 @@ class _ChatBubbleState extends State<ChatBubble> {
timeText, timeText,
if(widget.isSender) ...[ if(widget.isSender) ...[
SizedBox(width: widget.spacing), SizedBox(width: widget.spacing),
if(widget.isRead) Icon(
Icon(Icons.done_all_outlined, size: widget.timeIconSize, color: widget.timeIconColor) widget.isRead ? Icons.done_all_outlined: Icons.done_outlined,
else size: widget.timeIconSize,
Icon(Icons.done_outlined, size: widget.timeIconSize, color: widget.timeIconColor) color: widget.timeIconColor
)
] ]
], ],
) )
@ -390,6 +414,9 @@ class _ChatBubbleState extends State<ChatBubble> {
], ],
), ),
), ),
],
),
),
), ),
), ),
Visibility( Visibility(
@ -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,7 +95,34 @@ 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: [
Consumer<ChatProps>(
builder: (context, data, child) {
if(data.getReferenceMessageId != null) {
var referenceMessage = data.getChatResponse.sortByTimestamp().where((element) => element.id == data.getReferenceMessageId).last;
return Row(
children: [
Expanded(
child: AnswerReference(
context: context,
referenceMessage: referenceMessage,
selfId: widget.selfId,
),
),
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();
}
},
),
Row(
children: <Widget>[ children: <Widget>[
GestureDetector( GestureDetector(
onTap: (){ onTap: (){
@ -163,20 +194,24 @@ class _ChatTextfieldState extends State<ChatTextfield> {
const SizedBox(width: 15), const SizedBox(width: 15),
FloatingActionButton( FloatingActionButton(
mini: true, mini: true,
onPressed: (){ onPressed: () {
if(_textBoxController.text.isEmpty) return; if(_textBoxController.text.isEmpty) return;
if(isLoading) return; if(isLoading) return;
setState(() { setState(() {
isLoading = true; isLoading = true;
}); });
SendMessage(widget.sendToToken, SendMessageParams(_textBoxController.text)).run().then((value) { SendMessage(widget.sendToToken, SendMessageParams(
_textBoxController.text,
replyTo: Provider.of<ChatProps>(context, listen: false).getReferenceMessageId.toString()
)).run().then((value) {
_query(); _query();
setState(() { setState(() {
isLoading = false; isLoading = false;
}); });
_textBoxController.text = ''; _textBoxController.text = '';
setDraft(''); setDraft('');
Provider.of<ChatProps>(context, listen: false).setReferenceMessageId(null, context, widget.sendToToken);
}); });
}, },
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
@ -187,6 +222,9 @@ class _ChatTextfieldState extends State<ChatTextfield> {
), ),
], ],
), ),
],
),
), ),
), ),
], ],

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,