From 1ab87c6dd0ea0f7eb392ea6743c827244ba8e37f Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 26 Jun 2022 22:37:05 +0200 Subject: [PATCH 01/42] first draft for mention --- lib/views/home/chat/widgets/chat-input.dart | 188 ++++++++++-------- lib/views/home/chat/widgets/chat-mention.dart | 45 +++++ 2 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 lib/views/home/chat/widgets/chat-mention.dart diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 9d3582585..0fdcd37b9 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -21,10 +21,14 @@ import 'package:syphon/store/index.dart'; import 'package:syphon/store/rooms/room/model.dart'; import 'package:syphon/store/rooms/selectors.dart'; import 'package:syphon/store/settings/theme-settings/selectors.dart'; +import 'package:syphon/store/user/model.dart'; +import 'package:syphon/store/user/selectors.dart'; import 'package:syphon/views/widgets/buttons/button-text.dart'; import 'package:syphon/views/widgets/containers/media-card.dart'; import 'package:syphon/views/widgets/lists/list-local-images.dart'; +import 'chat-mention.dart'; + const DEFAULT_BORDER_RADIUS = 24.0; _empty({ @@ -450,101 +454,108 @@ class ChatInputState extends State { ), ), //////// TEXT FIELD //////// - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - constraints: BoxConstraints( - maxHeight: maxInputHeight, - maxWidth: messageInputWidth, - ), - child: Stack( - children: [ - Visibility( - visible: widget.editing, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ButtonText( - text: Strings.buttonSaveMessageEdit, - size: 18.0, - disabled: widget.sending || !isSendable, - onPressed: () => onSubmit(), - ), - ], - ), + Column( + children: [ + Visibility(visible: true, + child: Mention( + data: props.users,)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + constraints: BoxConstraints( + maxHeight: maxInputHeight, + maxWidth: messageInputWidth, ), - Visibility( - visible: !widget.editing, - child: TextField( - maxLines: null, - autocorrect: props.autocorrectEnabled, - enableSuggestions: props.suggestionsEnabled, - textCapitalization: props.textCapitalization, - keyboardType: TextInputType.multiline, - textInputAction: - widget.enterSend ? TextInputAction.send : TextInputAction.newline, - cursorColor: props.inputCursorColor, - focusNode: widget.focusNode, - controller: widget.controller, - onChanged: (text) => onUpdate(text, props: props), - onSubmitted: !isSendable ? null : (text) => onSubmit(), - style: TextStyle( - height: 1.5, - color: props.inputTextColor, - ), - decoration: InputDecoration( - filled: true, - hintText: hintText, - suffixIcon: Visibility( - visible: isSendable, - child: IconButton( - color: Theme.of(context).iconTheme.color, - onPressed: () => onToggleMediaOptions(), - icon: Icon( - Icons.add, - size: Dimensions.iconSizeLarge, + child: Stack( + children: [ + Visibility( + visible: widget.editing, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ButtonText( + text: Strings.buttonSaveMessageEdit, + size: 18.0, + disabled: widget.sending || !isSendable, + onPressed: () => onSubmit(), ), - ), + ], ), - fillColor: props.inputColorBackground, - contentPadding: Dimensions.inputContentPadding, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.secondary, - width: 1, + ), + Visibility( + visible: !widget.editing, + child: TextField( + maxLines: null, + autocorrect: props.autocorrectEnabled, + enableSuggestions: props.suggestionsEnabled, + textCapitalization: props.textCapitalization, + keyboardType: TextInputType.multiline, + textInputAction: + widget.enterSend ? TextInputAction.send : TextInputAction.newline, + cursorColor: props.inputCursorColor, + focusNode: widget.focusNode, + controller: widget.controller, + onChanged: (text) => onUpdate(text, props: props), + onSubmitted: !isSendable ? null : (text) => onSubmit(), + style: TextStyle( + height: 1.5, + color: props.inputTextColor, + ), + decoration: InputDecoration( + filled: true, + hintText: hintText, + suffixIcon: Visibility( + visible: isSendable, + child: IconButton( + color: Theme.of(context).iconTheme.color, + onPressed: () => onToggleMediaOptions(), + icon: Icon( + Icons.add, + size: Dimensions.iconSizeLarge, + ), + ), ), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - topRight: + fillColor: props.inputColorBackground, + contentPadding: Dimensions.inputContentPadding, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.secondary, + width: 1, + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), + topRight: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), - bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), - )), - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.secondary, - width: 1, - ), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - topRight: + bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), + bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), + )), + border: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.secondary, + width: 1, + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), + topRight: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), - bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), - )), + bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), + bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), + )), + ), + ), ), - ), + ], ), - ], - ), - ), - Container( - width: Dimensions.buttonSendSize, - padding: EdgeInsets.symmetric(vertical: 4), - child: sendButton, + ), + Container( + width: Dimensions.buttonSendSize, + padding: EdgeInsets.symmetric(vertical: 4), + child: sendButton, + ), + ], ), ], ), @@ -633,6 +644,7 @@ class _Props extends Equatable { final bool autocorrectEnabled; final bool suggestionsEnabled; final TextCapitalization textCapitalization; + final List users; final Function onSendTyping; @@ -646,6 +658,7 @@ class _Props extends Equatable { required this.suggestionsEnabled, required this.textCapitalization, required this.onSendTyping, + required this.users }); @override @@ -656,6 +669,7 @@ class _Props extends Equatable { static _Props mapStateToProps(Store store, String roomId) => _Props( room: selectRoom(id: roomId, state: store.state), + users: roomUsers(store.state, roomId), inputTextColor: selectInputTextColor(store.state.settingsStore.themeSettings.themeType), inputCursorColor: selectCursorColor(store.state.settingsStore.themeSettings.themeType), inputColorBackground: diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart new file mode 100644 index 000000000..80fc1c641 --- /dev/null +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:syphon/store/user/model.dart'; + + +class Mention extends StatefulWidget{ + + const Mention({ + Key? key, + required this.data + }) : super(key: key); + + final List data; + + @override + State createState() => MentionState(); +} + +class MentionState extends State{ + + List images = [ + "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", + "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", + ]; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemBuilder: (buildContext, index){ + return Card( + child: ListTile( + leading: CircleAvatar(backgroundImage: NetworkImage(images[index] + + ),), + title: Text('This is title'), + subtitle: Text('This is subtitle'), + ), + ); + }, + itemCount: images.length, + shrinkWrap: true, + scrollDirection: Axis.vertical, + ); + } +} + From 86cfbf0362f17761cf0d7ac136c31704a3dc6c28 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Mon, 27 Jun 2022 21:49:56 +0200 Subject: [PATCH 02/42] show mention added --- lib/views/home/chat/widgets/chat-input.dart | 38 +++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 0fdcd37b9..efd7a6f14 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -83,6 +83,9 @@ class ChatInput extends StatefulWidget { class ChatInputState extends State { ChatInputState() : super(); + bool mention = false; + List users = []; + bool sendable = false; bool showAttachments = false; @@ -145,6 +148,34 @@ class ChatInputState extends State { sendable = text.trim().isNotEmpty; }); + + // mention-dialog + mention = false; + users = props!.users; + + if (text.startsWith('@')){ + mention = true; + + setState((){ + users = users.where((user) { + if (user != null){ + final String searchText = text.toLowerCase(); + + if(user.userId != null){ + return user.userId!.toLowerCase().contains(searchText); + } + else { + return user.displayName!.toLowerCase().contains(searchText); + } + } + else{ + return false; + } + }).toList(); + }); + } + + // start an interval for updating typing status if (widget.focusNode.hasFocus && typingNotifier == null) { props!.onSendTyping(typing: true, roomId: props.room.id); @@ -456,9 +487,10 @@ class ChatInputState extends State { //////// TEXT FIELD //////// Column( children: [ - Visibility(visible: true, - child: Mention( - data: props.users,)), + Visibility(visible: showMention, + child: Container(padding: EdgeInsets.symmetric(horizontal: 10), width: MediaQuery.of(context).size.width + - Dimensions.buttonSendSize * 1.5 , child: Mention( + users: users,)),), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, From 5ff58720af11549ae536feb7340586d1bf3cfc70 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Mon, 27 Jun 2022 21:50:31 +0200 Subject: [PATCH 03/42] Avatar added for ListView --- lib/views/home/chat/widgets/chat-mention.dart | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 80fc1c641..b7c714d9d 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -1,15 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:syphon/global/colors.dart'; +import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/user/model.dart'; +import 'package:syphon/store/user/selectors.dart'; +import 'package:syphon/views/widgets/avatars/avatar.dart'; class Mention extends StatefulWidget{ const Mention({ Key? key, - required this.data + required this.users }) : super(key: key); - final List data; + final List users; @override State createState() => MentionState(); @@ -17,28 +21,31 @@ class Mention extends StatefulWidget{ class MentionState extends State{ - List images = [ - "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", - "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", - ]; @override Widget build(BuildContext context) { return ListView.builder( itemBuilder: (buildContext, index){ + final String userName = formatUsername(widget.users[index] as User); + return Card( child: ListTile( - leading: CircleAvatar(backgroundImage: NetworkImage(images[index] - - ),), - title: Text('This is title'), - subtitle: Text('This is subtitle'), + leading: Avatar( + uri: widget.users[index]?.avatarUri, + alt: userName, + size: Dimensions.avatarSizeMin, + background: AppColors.hashedColor( + userName, + ),), + title: Text(userName), + subtitle: Text(widget.users[index]?.userId), ), ); }, - itemCount: images.length, + itemCount: widget.users.length, shrinkWrap: true, scrollDirection: Axis.vertical, + padding: EdgeInsets.symmetric(vertical: 5), ); } } From db46b4d11560617bbf9b3148e89187a17dc3ab68 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Mon, 27 Jun 2022 21:50:52 +0200 Subject: [PATCH 04/42] crossAxisAlignment added --- lib/views/home/chat/widgets/chat-input.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index efd7a6f14..178a71877 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -300,6 +300,9 @@ class ChatInputState extends State { // account for if editing widget.editing && (widget.editorController?.text.isNotEmpty ?? false); + final bool showMention = mention; + + Color sendButtonColor = const Color(AppColors.blueBubbly); if (widget.mediumType == MediumType.plaintext) { @@ -486,6 +489,7 @@ class ChatInputState extends State { ), //////// TEXT FIELD //////// Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Visibility(visible: showMention, child: Container(padding: EdgeInsets.symmetric(horizontal: 10), width: MediaQuery.of(context).size.width From c49d44e2bdeb1712313ab66659430ab4ad7beca9 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Tue, 28 Jun 2022 04:17:44 +0200 Subject: [PATCH 05/42] fixed overflow --- lib/views/home/chat/widgets/chat-mention.dart | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index b7c714d9d..4cb080e9d 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -24,29 +24,32 @@ class MentionState extends State{ @override Widget build(BuildContext context) { - return ListView.builder( - itemBuilder: (buildContext, index){ - final String userName = formatUsername(widget.users[index] as User); - - return Card( - child: ListTile( - leading: Avatar( - uri: widget.users[index]?.avatarUri, - alt: userName, - size: Dimensions.avatarSizeMin, - background: AppColors.hashedColor( - userName, - ),), - title: Text(userName), - subtitle: Text(widget.users[index]?.userId), - ), - ); - }, - itemCount: widget.users.length, - shrinkWrap: true, - scrollDirection: Axis.vertical, - padding: EdgeInsets.symmetric(vertical: 5), - ); + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: 200.0, + ), + child: ListView.builder(itemBuilder: (buildContext, index){ + final String userName = formatUsername(widget.users[index] as User); + return Card( + child: ListTile( + leading: Avatar( + uri: widget.users[index]?.avatarUri, + alt: userName, + size: Dimensions.avatarSizeMin, + background: AppColors.hashedColor( + userName, + ),), + title: Text(userName), + subtitle: Text(widget.users[index]?.userId), + ), + ); + }, + itemCount: widget.users.length, + shrinkWrap: true, + scrollDirection: Axis.vertical, + physics: ClampingScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 5), + )); } } From 63c7dfe9e1a227a47a00dd640b6d46195c15ebdc Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Tue, 28 Jun 2022 17:48:05 +0200 Subject: [PATCH 06/42] moved width to Mention --- lib/views/home/chat/widgets/chat-input.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 178a71877..1d844df06 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -492,9 +492,13 @@ class ChatInputState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Visibility(visible: showMention, - child: Container(padding: EdgeInsets.symmetric(horizontal: 10), width: MediaQuery.of(context).size.width - - Dimensions.buttonSendSize * 1.5 , child: Mention( - users: users,)),), + child: Container(padding: EdgeInsets.symmetric(horizontal: 10), + child: Mention( + width: MediaQuery.of(context).size.width + - Dimensions.buttonSendSize * 2, + users: users, + controller: widget.controller,)), + ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, From dc22b30e0133ccae0ca80ff44309fe9dcae1b3a8 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Tue, 28 Jun 2022 17:48:45 +0200 Subject: [PATCH 07/42] if a item user is clicked now the textField is updated --- lib/views/home/chat/widgets/chat-mention.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 4cb080e9d..508a63cae 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -10,10 +10,17 @@ class Mention extends StatefulWidget{ const Mention({ Key? key, - required this.users + required this.users, + required this.width, + required this.controller, + this.height = 200 }) : super(key: key); final List users; + final double width; + final double height; + final TextEditingController controller; + @override State createState() => MentionState(); @@ -26,12 +33,14 @@ class MentionState extends State{ Widget build(BuildContext context) { return ConstrainedBox( constraints: BoxConstraints( - maxHeight: 200.0, + maxHeight: widget.height, + maxWidth: widget.width ), child: ListView.builder(itemBuilder: (buildContext, index){ final String userName = formatUsername(widget.users[index] as User); return Card( child: ListTile( + onTap: () => onTab(widget.users[index]), leading: Avatar( uri: widget.users[index]?.avatarUri, alt: userName, @@ -51,5 +60,9 @@ class MentionState extends State{ padding: EdgeInsets.symmetric(vertical: 5), )); } + + onTab(User user){ + widget.controller.text = user.userId!; + } } From d7b1f3776729c95b582f1516d36d2e067858de37 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 2 Jul 2022 15:41:19 +0200 Subject: [PATCH 08/42] Cursor now moved to end --- lib/views/home/chat/widgets/chat-mention.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 508a63cae..20aa25618 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -63,6 +63,8 @@ class MentionState extends State{ onTab(User user){ widget.controller.text = user.userId!; + widget.controller.selection = TextSelection.fromPosition( + TextPosition(offset: widget.controller.text.length)); } } From 9b786968a3248649a759157b6bacfe8882fb34b7 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sat, 25 Jun 2022 14:23:26 +0100 Subject: [PATCH 09/42] Fix extended profile widget --- lib/views/home/profile/profile-user-screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/views/home/profile/profile-user-screen.dart b/lib/views/home/profile/profile-user-screen.dart index 1cb6b6126..b6a159fa1 100644 --- a/lib/views/home/profile/profile-user-screen.dart +++ b/lib/views/home/profile/profile-user-screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:syphon/global/colors.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/hooks.dart'; +import 'package:syphon/store/index.dart'; import 'package:syphon/store/user/actions.dart'; import 'package:syphon/store/user/model.dart'; import 'package:syphon/views/navigation.dart'; @@ -20,7 +21,7 @@ class UserProfileArguments { class UserProfileScreen extends HookWidget { @override Widget build(BuildContext context) { - final dispatch = useDispatch(); + final dispatch = useDispatch(); final arguments = useScreenArguments(context)!; final headerOpacity = useState(1.0); From daf7998341c47015a3b291b7005340ee5fde4108 Mon Sep 17 00:00:00 2001 From: taylor Date: Fri, 1 Jul 2022 11:51:18 -0400 Subject: [PATCH 10/42] Update styleguide.md --- docs/styleguide.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/styleguide.md b/docs/styleguide.md index 0dda43482..44e862fc7 100644 --- a/docs/styleguide.md +++ b/docs/styleguide.md @@ -8,9 +8,9 @@ Note these are also, as most things in software, largely based on subjective opi The primary goal of this document is not to impose arbitrary restrictions, but to make writing and maintaining Syphon *simple, accessible, and fun*. We want everyone to be able to meaningfully contribute to the code base no matter your skill level. Let us know what else we can do to continue futher this goal! :) -### Akways define UI elements outside the ViewModel +### Always define UI elements outside the ViewModel -- DO +- `DO` ```dart @@ -43,7 +43,7 @@ class IntroSettingsScreen extends StatelessWidget { } ``` -- DON'T +- `DONT` ```dart From c9d1ee0e30feb71b1d59cc6ca6303a2b32413dfd Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 2 Jul 2022 21:40:59 +0200 Subject: [PATCH 11/42] fixed visibility --- lib/views/home/chat/widgets/chat-input.dart | 29 +++++++------ lib/views/home/chat/widgets/chat-mention.dart | 41 +++++++++++-------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 1d844df06..8ed3f5cb6 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -150,13 +150,12 @@ class ChatInputState extends State { // mention-dialog - mention = false; - users = props!.users; + setState((){ + mention = false; + users = props!.users; - if (text.startsWith('@')){ - mention = true; - - setState((){ + if (text.startsWith('@')){ + mention = true; users = users.where((user) { if (user != null){ final String searchText = text.toLowerCase(); @@ -172,8 +171,8 @@ class ChatInputState extends State { return false; } }).toList(); - }); - } + } + }); // start an interval for updating typing status @@ -491,13 +490,13 @@ class ChatInputState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Visibility(visible: showMention, - child: Container(padding: EdgeInsets.symmetric(horizontal: 10), - child: Mention( - width: MediaQuery.of(context).size.width - - Dimensions.buttonSendSize * 2, - users: users, - controller: widget.controller,)), + Container(padding: EdgeInsets.symmetric(horizontal: 10), + child: Mention( + visible: showMention, + width: MediaQuery.of(context).size.width - Dimensions.buttonSendSize * 2, + users: users, + controller: widget.controller, + ) ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 20aa25618..1bf14bffd 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -8,11 +8,12 @@ import 'package:syphon/views/widgets/avatars/avatar.dart'; class Mention extends StatefulWidget{ - const Mention({ + Mention({ Key? key, required this.users, required this.width, required this.controller, + this.visible = false, this.height = 200 }) : super(key: key); @@ -20,6 +21,7 @@ class Mention extends StatefulWidget{ final double width; final double height; final TextEditingController controller; + bool visible; @override @@ -38,20 +40,22 @@ class MentionState extends State{ ), child: ListView.builder(itemBuilder: (buildContext, index){ final String userName = formatUsername(widget.users[index] as User); - return Card( - child: ListTile( - onTap: () => onTab(widget.users[index]), - leading: Avatar( - uri: widget.users[index]?.avatarUri, - alt: userName, - size: Dimensions.avatarSizeMin, - background: AppColors.hashedColor( - userName, - ),), - title: Text(userName), - subtitle: Text(widget.users[index]?.userId), + return Visibility( + visible: widget.visible, + child:Card( + child: ListTile( + onTap: () => onTab(widget.users[index]), + leading: Avatar( + uri: widget.users[index]?.avatarUri, + alt: userName, + size: Dimensions.avatarSizeMin, + background: AppColors.hashedColor( + userName, + ),), + title: Text(userName), + subtitle: Text(widget.users[index]?.userId), ), - ); + )); }, itemCount: widget.users.length, shrinkWrap: true, @@ -62,9 +66,12 @@ class MentionState extends State{ } onTab(User user){ - widget.controller.text = user.userId!; - widget.controller.selection = TextSelection.fromPosition( - TextPosition(offset: widget.controller.text.length)); + setState((){ + widget.controller.text = user.userId!; + widget.controller.selection = TextSelection.fromPosition( + TextPosition(offset: widget.controller.text.length)); + widget.visible = false; + }); } } From d854e7cc2a621cdaadb72d8b5d4dc26f34a9ed82 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 24 Jul 2022 13:05:50 +0200 Subject: [PATCH 12/42] fixed bug with mention on hitting backspace for mor information see https://github.com/syphon-org/syphon/pull/706#discussion_r922816851 --- lib/views/home/chat/widgets/chat-input.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 8ed3f5cb6..c9964d16b 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -172,6 +172,9 @@ class ChatInputState extends State { } }).toList(); } + else{ + users = []; + } }); From 245b2804d7e1fd357686a160b1436580a4a56df4 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 24 Jul 2022 13:06:29 +0200 Subject: [PATCH 13/42] mention now also possible in message body --- lib/views/home/chat/widgets/chat-input.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index c9964d16b..9b8945a16 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -154,11 +154,12 @@ class ChatInputState extends State { mention = false; users = props!.users; - if (text.startsWith('@')){ + if (text.startsWith('@') || text.contains(' @')) { mention = true; users = users.where((user) { if (user != null){ - final String searchText = text.toLowerCase(); + final String searchText = text.toLowerCase() + .substring(text.indexOf('@') + 1, text.length); if(user.userId != null){ return user.userId!.toLowerCase().contains(searchText); From d072710cf1347e548ce5512887876d79d8018153 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 26 Jun 2022 22:37:05 +0200 Subject: [PATCH 14/42] first draft for mention --- lib/views/home/chat/widgets/chat-input.dart | 188 ++++++++++-------- lib/views/home/chat/widgets/chat-mention.dart | 45 +++++ 2 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 lib/views/home/chat/widgets/chat-mention.dart diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 9d3582585..0fdcd37b9 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -21,10 +21,14 @@ import 'package:syphon/store/index.dart'; import 'package:syphon/store/rooms/room/model.dart'; import 'package:syphon/store/rooms/selectors.dart'; import 'package:syphon/store/settings/theme-settings/selectors.dart'; +import 'package:syphon/store/user/model.dart'; +import 'package:syphon/store/user/selectors.dart'; import 'package:syphon/views/widgets/buttons/button-text.dart'; import 'package:syphon/views/widgets/containers/media-card.dart'; import 'package:syphon/views/widgets/lists/list-local-images.dart'; +import 'chat-mention.dart'; + const DEFAULT_BORDER_RADIUS = 24.0; _empty({ @@ -450,101 +454,108 @@ class ChatInputState extends State { ), ), //////// TEXT FIELD //////// - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - constraints: BoxConstraints( - maxHeight: maxInputHeight, - maxWidth: messageInputWidth, - ), - child: Stack( - children: [ - Visibility( - visible: widget.editing, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ButtonText( - text: Strings.buttonSaveMessageEdit, - size: 18.0, - disabled: widget.sending || !isSendable, - onPressed: () => onSubmit(), - ), - ], - ), + Column( + children: [ + Visibility(visible: true, + child: Mention( + data: props.users,)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + constraints: BoxConstraints( + maxHeight: maxInputHeight, + maxWidth: messageInputWidth, ), - Visibility( - visible: !widget.editing, - child: TextField( - maxLines: null, - autocorrect: props.autocorrectEnabled, - enableSuggestions: props.suggestionsEnabled, - textCapitalization: props.textCapitalization, - keyboardType: TextInputType.multiline, - textInputAction: - widget.enterSend ? TextInputAction.send : TextInputAction.newline, - cursorColor: props.inputCursorColor, - focusNode: widget.focusNode, - controller: widget.controller, - onChanged: (text) => onUpdate(text, props: props), - onSubmitted: !isSendable ? null : (text) => onSubmit(), - style: TextStyle( - height: 1.5, - color: props.inputTextColor, - ), - decoration: InputDecoration( - filled: true, - hintText: hintText, - suffixIcon: Visibility( - visible: isSendable, - child: IconButton( - color: Theme.of(context).iconTheme.color, - onPressed: () => onToggleMediaOptions(), - icon: Icon( - Icons.add, - size: Dimensions.iconSizeLarge, + child: Stack( + children: [ + Visibility( + visible: widget.editing, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ButtonText( + text: Strings.buttonSaveMessageEdit, + size: 18.0, + disabled: widget.sending || !isSendable, + onPressed: () => onSubmit(), ), - ), + ], ), - fillColor: props.inputColorBackground, - contentPadding: Dimensions.inputContentPadding, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.secondary, - width: 1, + ), + Visibility( + visible: !widget.editing, + child: TextField( + maxLines: null, + autocorrect: props.autocorrectEnabled, + enableSuggestions: props.suggestionsEnabled, + textCapitalization: props.textCapitalization, + keyboardType: TextInputType.multiline, + textInputAction: + widget.enterSend ? TextInputAction.send : TextInputAction.newline, + cursorColor: props.inputCursorColor, + focusNode: widget.focusNode, + controller: widget.controller, + onChanged: (text) => onUpdate(text, props: props), + onSubmitted: !isSendable ? null : (text) => onSubmit(), + style: TextStyle( + height: 1.5, + color: props.inputTextColor, + ), + decoration: InputDecoration( + filled: true, + hintText: hintText, + suffixIcon: Visibility( + visible: isSendable, + child: IconButton( + color: Theme.of(context).iconTheme.color, + onPressed: () => onToggleMediaOptions(), + icon: Icon( + Icons.add, + size: Dimensions.iconSizeLarge, + ), + ), ), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - topRight: + fillColor: props.inputColorBackground, + contentPadding: Dimensions.inputContentPadding, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.secondary, + width: 1, + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), + topRight: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), - bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), - )), - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.secondary, - width: 1, - ), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - topRight: + bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), + bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), + )), + border: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.secondary, + width: 1, + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), + topRight: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), - bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), - )), + bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), + bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), + )), + ), + ), ), - ), + ], ), - ], - ), - ), - Container( - width: Dimensions.buttonSendSize, - padding: EdgeInsets.symmetric(vertical: 4), - child: sendButton, + ), + Container( + width: Dimensions.buttonSendSize, + padding: EdgeInsets.symmetric(vertical: 4), + child: sendButton, + ), + ], ), ], ), @@ -633,6 +644,7 @@ class _Props extends Equatable { final bool autocorrectEnabled; final bool suggestionsEnabled; final TextCapitalization textCapitalization; + final List users; final Function onSendTyping; @@ -646,6 +658,7 @@ class _Props extends Equatable { required this.suggestionsEnabled, required this.textCapitalization, required this.onSendTyping, + required this.users }); @override @@ -656,6 +669,7 @@ class _Props extends Equatable { static _Props mapStateToProps(Store store, String roomId) => _Props( room: selectRoom(id: roomId, state: store.state), + users: roomUsers(store.state, roomId), inputTextColor: selectInputTextColor(store.state.settingsStore.themeSettings.themeType), inputCursorColor: selectCursorColor(store.state.settingsStore.themeSettings.themeType), inputColorBackground: diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart new file mode 100644 index 000000000..80fc1c641 --- /dev/null +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:syphon/store/user/model.dart'; + + +class Mention extends StatefulWidget{ + + const Mention({ + Key? key, + required this.data + }) : super(key: key); + + final List data; + + @override + State createState() => MentionState(); +} + +class MentionState extends State{ + + List images = [ + "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", + "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", + ]; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemBuilder: (buildContext, index){ + return Card( + child: ListTile( + leading: CircleAvatar(backgroundImage: NetworkImage(images[index] + + ),), + title: Text('This is title'), + subtitle: Text('This is subtitle'), + ), + ); + }, + itemCount: images.length, + shrinkWrap: true, + scrollDirection: Axis.vertical, + ); + } +} + From 815f228e730e7375653e77e21e3afe844c813c9d Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Mon, 27 Jun 2022 21:49:56 +0200 Subject: [PATCH 15/42] show mention added --- lib/views/home/chat/widgets/chat-input.dart | 38 +++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 0fdcd37b9..efd7a6f14 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -83,6 +83,9 @@ class ChatInput extends StatefulWidget { class ChatInputState extends State { ChatInputState() : super(); + bool mention = false; + List users = []; + bool sendable = false; bool showAttachments = false; @@ -145,6 +148,34 @@ class ChatInputState extends State { sendable = text.trim().isNotEmpty; }); + + // mention-dialog + mention = false; + users = props!.users; + + if (text.startsWith('@')){ + mention = true; + + setState((){ + users = users.where((user) { + if (user != null){ + final String searchText = text.toLowerCase(); + + if(user.userId != null){ + return user.userId!.toLowerCase().contains(searchText); + } + else { + return user.displayName!.toLowerCase().contains(searchText); + } + } + else{ + return false; + } + }).toList(); + }); + } + + // start an interval for updating typing status if (widget.focusNode.hasFocus && typingNotifier == null) { props!.onSendTyping(typing: true, roomId: props.room.id); @@ -456,9 +487,10 @@ class ChatInputState extends State { //////// TEXT FIELD //////// Column( children: [ - Visibility(visible: true, - child: Mention( - data: props.users,)), + Visibility(visible: showMention, + child: Container(padding: EdgeInsets.symmetric(horizontal: 10), width: MediaQuery.of(context).size.width + - Dimensions.buttonSendSize * 1.5 , child: Mention( + users: users,)),), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, From d63a56619acaee6c4b68ff808bb2fe3002dceadf Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Mon, 27 Jun 2022 21:50:31 +0200 Subject: [PATCH 16/42] Avatar added for ListView --- lib/views/home/chat/widgets/chat-mention.dart | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 80fc1c641..b7c714d9d 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -1,15 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:syphon/global/colors.dart'; +import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/user/model.dart'; +import 'package:syphon/store/user/selectors.dart'; +import 'package:syphon/views/widgets/avatars/avatar.dart'; class Mention extends StatefulWidget{ const Mention({ Key? key, - required this.data + required this.users }) : super(key: key); - final List data; + final List users; @override State createState() => MentionState(); @@ -17,28 +21,31 @@ class Mention extends StatefulWidget{ class MentionState extends State{ - List images = [ - "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", - "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", - ]; @override Widget build(BuildContext context) { return ListView.builder( itemBuilder: (buildContext, index){ + final String userName = formatUsername(widget.users[index] as User); + return Card( child: ListTile( - leading: CircleAvatar(backgroundImage: NetworkImage(images[index] - - ),), - title: Text('This is title'), - subtitle: Text('This is subtitle'), + leading: Avatar( + uri: widget.users[index]?.avatarUri, + alt: userName, + size: Dimensions.avatarSizeMin, + background: AppColors.hashedColor( + userName, + ),), + title: Text(userName), + subtitle: Text(widget.users[index]?.userId), ), ); }, - itemCount: images.length, + itemCount: widget.users.length, shrinkWrap: true, scrollDirection: Axis.vertical, + padding: EdgeInsets.symmetric(vertical: 5), ); } } From 6f956fb041c9f40cd8b8b1b69d6665ea8332df77 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Mon, 27 Jun 2022 21:50:52 +0200 Subject: [PATCH 17/42] crossAxisAlignment added --- lib/views/home/chat/widgets/chat-input.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index efd7a6f14..178a71877 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -300,6 +300,9 @@ class ChatInputState extends State { // account for if editing widget.editing && (widget.editorController?.text.isNotEmpty ?? false); + final bool showMention = mention; + + Color sendButtonColor = const Color(AppColors.blueBubbly); if (widget.mediumType == MediumType.plaintext) { @@ -486,6 +489,7 @@ class ChatInputState extends State { ), //////// TEXT FIELD //////// Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Visibility(visible: showMention, child: Container(padding: EdgeInsets.symmetric(horizontal: 10), width: MediaQuery.of(context).size.width From 490a401001ab9e5c53616d2c517ff1be52039a25 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Tue, 28 Jun 2022 04:17:44 +0200 Subject: [PATCH 18/42] fixed overflow --- lib/views/home/chat/widgets/chat-mention.dart | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index b7c714d9d..4cb080e9d 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -24,29 +24,32 @@ class MentionState extends State{ @override Widget build(BuildContext context) { - return ListView.builder( - itemBuilder: (buildContext, index){ - final String userName = formatUsername(widget.users[index] as User); - - return Card( - child: ListTile( - leading: Avatar( - uri: widget.users[index]?.avatarUri, - alt: userName, - size: Dimensions.avatarSizeMin, - background: AppColors.hashedColor( - userName, - ),), - title: Text(userName), - subtitle: Text(widget.users[index]?.userId), - ), - ); - }, - itemCount: widget.users.length, - shrinkWrap: true, - scrollDirection: Axis.vertical, - padding: EdgeInsets.symmetric(vertical: 5), - ); + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: 200.0, + ), + child: ListView.builder(itemBuilder: (buildContext, index){ + final String userName = formatUsername(widget.users[index] as User); + return Card( + child: ListTile( + leading: Avatar( + uri: widget.users[index]?.avatarUri, + alt: userName, + size: Dimensions.avatarSizeMin, + background: AppColors.hashedColor( + userName, + ),), + title: Text(userName), + subtitle: Text(widget.users[index]?.userId), + ), + ); + }, + itemCount: widget.users.length, + shrinkWrap: true, + scrollDirection: Axis.vertical, + physics: ClampingScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 5), + )); } } From f952636159aa0a94ef4b4e24d1811c2cc0390806 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Tue, 28 Jun 2022 17:48:05 +0200 Subject: [PATCH 19/42] moved width to Mention --- lib/views/home/chat/widgets/chat-input.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 178a71877..1d844df06 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -492,9 +492,13 @@ class ChatInputState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Visibility(visible: showMention, - child: Container(padding: EdgeInsets.symmetric(horizontal: 10), width: MediaQuery.of(context).size.width - - Dimensions.buttonSendSize * 1.5 , child: Mention( - users: users,)),), + child: Container(padding: EdgeInsets.symmetric(horizontal: 10), + child: Mention( + width: MediaQuery.of(context).size.width + - Dimensions.buttonSendSize * 2, + users: users, + controller: widget.controller,)), + ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, From a371fd849a7a078cfb96e6ea958d47cd4e2a3355 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Tue, 28 Jun 2022 17:48:45 +0200 Subject: [PATCH 20/42] if a item user is clicked now the textField is updated --- lib/views/home/chat/widgets/chat-mention.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 4cb080e9d..508a63cae 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -10,10 +10,17 @@ class Mention extends StatefulWidget{ const Mention({ Key? key, - required this.users + required this.users, + required this.width, + required this.controller, + this.height = 200 }) : super(key: key); final List users; + final double width; + final double height; + final TextEditingController controller; + @override State createState() => MentionState(); @@ -26,12 +33,14 @@ class MentionState extends State{ Widget build(BuildContext context) { return ConstrainedBox( constraints: BoxConstraints( - maxHeight: 200.0, + maxHeight: widget.height, + maxWidth: widget.width ), child: ListView.builder(itemBuilder: (buildContext, index){ final String userName = formatUsername(widget.users[index] as User); return Card( child: ListTile( + onTap: () => onTab(widget.users[index]), leading: Avatar( uri: widget.users[index]?.avatarUri, alt: userName, @@ -51,5 +60,9 @@ class MentionState extends State{ padding: EdgeInsets.symmetric(vertical: 5), )); } + + onTab(User user){ + widget.controller.text = user.userId!; + } } From 4faf36e122560ed01c3f14beaf4f0e3bf29ffb15 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 2 Jul 2022 15:41:19 +0200 Subject: [PATCH 21/42] Cursor now moved to end --- lib/views/home/chat/widgets/chat-mention.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 508a63cae..20aa25618 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -63,6 +63,8 @@ class MentionState extends State{ onTab(User user){ widget.controller.text = user.userId!; + widget.controller.selection = TextSelection.fromPosition( + TextPosition(offset: widget.controller.text.length)); } } From 0284dc129b44dbdef1d056f294a79893c21de48c Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 2 Jul 2022 21:40:59 +0200 Subject: [PATCH 22/42] fixed visibility --- lib/views/home/chat/widgets/chat-input.dart | 29 +++++++------ lib/views/home/chat/widgets/chat-mention.dart | 41 +++++++++++-------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 1d844df06..8ed3f5cb6 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -150,13 +150,12 @@ class ChatInputState extends State { // mention-dialog - mention = false; - users = props!.users; + setState((){ + mention = false; + users = props!.users; - if (text.startsWith('@')){ - mention = true; - - setState((){ + if (text.startsWith('@')){ + mention = true; users = users.where((user) { if (user != null){ final String searchText = text.toLowerCase(); @@ -172,8 +171,8 @@ class ChatInputState extends State { return false; } }).toList(); - }); - } + } + }); // start an interval for updating typing status @@ -491,13 +490,13 @@ class ChatInputState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Visibility(visible: showMention, - child: Container(padding: EdgeInsets.symmetric(horizontal: 10), - child: Mention( - width: MediaQuery.of(context).size.width - - Dimensions.buttonSendSize * 2, - users: users, - controller: widget.controller,)), + Container(padding: EdgeInsets.symmetric(horizontal: 10), + child: Mention( + visible: showMention, + width: MediaQuery.of(context).size.width - Dimensions.buttonSendSize * 2, + users: users, + controller: widget.controller, + ) ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 20aa25618..1bf14bffd 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -8,11 +8,12 @@ import 'package:syphon/views/widgets/avatars/avatar.dart'; class Mention extends StatefulWidget{ - const Mention({ + Mention({ Key? key, required this.users, required this.width, required this.controller, + this.visible = false, this.height = 200 }) : super(key: key); @@ -20,6 +21,7 @@ class Mention extends StatefulWidget{ final double width; final double height; final TextEditingController controller; + bool visible; @override @@ -38,20 +40,22 @@ class MentionState extends State{ ), child: ListView.builder(itemBuilder: (buildContext, index){ final String userName = formatUsername(widget.users[index] as User); - return Card( - child: ListTile( - onTap: () => onTab(widget.users[index]), - leading: Avatar( - uri: widget.users[index]?.avatarUri, - alt: userName, - size: Dimensions.avatarSizeMin, - background: AppColors.hashedColor( - userName, - ),), - title: Text(userName), - subtitle: Text(widget.users[index]?.userId), + return Visibility( + visible: widget.visible, + child:Card( + child: ListTile( + onTap: () => onTab(widget.users[index]), + leading: Avatar( + uri: widget.users[index]?.avatarUri, + alt: userName, + size: Dimensions.avatarSizeMin, + background: AppColors.hashedColor( + userName, + ),), + title: Text(userName), + subtitle: Text(widget.users[index]?.userId), ), - ); + )); }, itemCount: widget.users.length, shrinkWrap: true, @@ -62,9 +66,12 @@ class MentionState extends State{ } onTab(User user){ - widget.controller.text = user.userId!; - widget.controller.selection = TextSelection.fromPosition( - TextPosition(offset: widget.controller.text.length)); + setState((){ + widget.controller.text = user.userId!; + widget.controller.selection = TextSelection.fromPosition( + TextPosition(offset: widget.controller.text.length)); + widget.visible = false; + }); } } From f27962c81edcaef373a4e7a8c5082c485f03da47 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 24 Jul 2022 13:05:50 +0200 Subject: [PATCH 23/42] fixed bug with mention on hitting backspace for mor information see https://github.com/syphon-org/syphon/pull/706#discussion_r922816851 --- lib/views/home/chat/widgets/chat-input.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 8ed3f5cb6..c9964d16b 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -172,6 +172,9 @@ class ChatInputState extends State { } }).toList(); } + else{ + users = []; + } }); From 35c29c769c777ef6cb415103655e1a6a758d4612 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 24 Jul 2022 13:06:29 +0200 Subject: [PATCH 24/42] mention now also possible in message body --- lib/views/home/chat/widgets/chat-input.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index c9964d16b..9b8945a16 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -154,11 +154,12 @@ class ChatInputState extends State { mention = false; users = props!.users; - if (text.startsWith('@')){ + if (text.startsWith('@') || text.contains(' @')) { mention = true; users = users.where((user) { if (user != null){ - final String searchText = text.toLowerCase(); + final String searchText = text.toLowerCase() + .substring(text.indexOf('@') + 1, text.length); if(user.userId != null){ return user.userId!.toLowerCase().contains(searchText); From 832cef7af8bfbb206394580ff28894dee4f9b747 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 24 Jul 2022 22:28:35 +0200 Subject: [PATCH 25/42] fixed reply position --- lib/views/home/chat/widgets/chat-input.dart | 19 +++++++++++-------- lib/views/home/chat/widgets/chat-mention.dart | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 9b8945a16..6e8240cc1 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -414,7 +414,18 @@ class ChatInputState extends State { } return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ + //////// MENTION DIALOG //////// + Container(padding: EdgeInsets.symmetric(horizontal: 15, vertical: 5), + child: Mention( + visible: showMention, + width: MediaQuery.of(context).size.width - Dimensions.buttonSendSize * 2.1, // HACK: fix the width of the dialog + users: users, + controller: widget.controller, + ) + ), Visibility( visible: replying, maintainSize: false, @@ -494,14 +505,6 @@ class ChatInputState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container(padding: EdgeInsets.symmetric(horizontal: 10), - child: Mention( - visible: showMention, - width: MediaQuery.of(context).size.width - Dimensions.buttonSendSize * 2, - users: users, - controller: widget.controller, - ) - ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 1bf14bffd..1862de595 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -42,7 +42,7 @@ class MentionState extends State{ final String userName = formatUsername(widget.users[index] as User); return Visibility( visible: widget.visible, - child:Card( + child: Card( child: ListTile( onTap: () => onTab(widget.users[index]), leading: Avatar( From 55c57ad873c6c54fc0e7b450b35af7a0bc47f0ae Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 7 Aug 2022 09:17:11 +0200 Subject: [PATCH 26/42] fixed bug --- lib/views/home/chat/widgets/chat-mention.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 1862de595..926236444 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -67,7 +67,7 @@ class MentionState extends State{ onTab(User user){ setState((){ - widget.controller.text = user.userId!; + widget.controller.text = '${widget.controller.text} ${user.userId}'; widget.controller.selection = TextSelection.fromPosition( TextPosition(offset: widget.controller.text.length)); widget.visible = false; From c1e3ef44413687947632d8f63cbbc8a0cb72a627 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 7 Aug 2022 09:20:57 +0200 Subject: [PATCH 27/42] fixed formatting --- lib/views/home/chat/widgets/chat-input.dart | 173 +++++++++++------- lib/views/home/chat/widgets/chat-mention.dart | 88 +++++---- 2 files changed, 145 insertions(+), 116 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 6e8240cc1..70fdd34d8 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -148,37 +148,33 @@ class ChatInputState extends State { sendable = text.trim().isNotEmpty; }); - // mention-dialog - setState((){ + setState(() { mention = false; users = props!.users; if (text.startsWith('@') || text.contains(' @')) { mention = true; users = users.where((user) { - if (user != null){ - final String searchText = text.toLowerCase() + if (user != null) { + final String searchText = text + .toLowerCase() .substring(text.indexOf('@') + 1, text.length); - if(user.userId != null){ + if (user.userId != null) { return user.userId!.toLowerCase().contains(searchText); - } - else { + } else { return user.displayName!.toLowerCase().contains(searchText); } - } - else{ + } else { return false; } }).toList(); - } - else{ + } else { users = []; } }); - // start an interval for updating typing status if (widget.focusNode.hasFocus && typingNotifier == null) { props!.onSendTyping(typing: true, roomId: props.room.id); @@ -282,7 +278,8 @@ class ChatInputState extends State { @override Widget build(BuildContext context) => StoreConnector( distinct: true, - converter: (Store store) => _Props.mapStateToProps(store, widget.roomId), + converter: (Store store) => + _Props.mapStateToProps(store, widget.roomId), onInitialBuild: onMounted, builder: (context, props) { final double width = MediaQuery.of(context).size.width; @@ -291,21 +288,25 @@ class ChatInputState extends State { // dynamic dimensions final double messageInputWidth = width - 72; - final bool replying = widget.quotable != null && widget.quotable!.sender != null; + final bool replying = + widget.quotable != null && widget.quotable!.sender != null; final bool loading = widget.sending; - final double maxInputHeight = replying ? height * 0.45 : height * 0.65; - final double maxMediaHeight = keyboardHeight > 0 ? keyboardHeight : height * 0.38; + final double maxInputHeight = + replying ? height * 0.45 : height * 0.65; + final double maxMediaHeight = + keyboardHeight > 0 ? keyboardHeight : height * 0.38; - final imageHeight = - keyboardHeight > 0 ? maxMediaHeight * 0.65 : imageWidth; // 2 images in view + final imageHeight = keyboardHeight > 0 + ? maxMediaHeight * 0.65 + : imageWidth; // 2 images in view final isSendable = (sendable && !widget.sending) || // account for if editing - widget.editing && (widget.editorController?.text.isNotEmpty ?? false); + widget.editing && + (widget.editorController?.text.isNotEmpty ?? false); final bool showMention = mention; - Color sendButtonColor = const Color(AppColors.blueBubbly); if (widget.mediumType == MediumType.plaintext) { @@ -418,14 +419,16 @@ class ChatInputState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ //////// MENTION DIALOG //////// - Container(padding: EdgeInsets.symmetric(horizontal: 15, vertical: 5), + Container( + padding: EdgeInsets.symmetric(horizontal: 15, vertical: 5), child: Mention( visible: showMention, - width: MediaQuery.of(context).size.width - Dimensions.buttonSendSize * 2.1, // HACK: fix the width of the dialog + width: MediaQuery.of(context).size.width - + Dimensions.buttonSendSize * + 2.1, // HACK: fix the width of the dialog users: users, controller: widget.controller, - ) - ), + )), Visibility( visible: replying, maintainSize: false, @@ -453,31 +456,41 @@ class ChatInputState extends State { ), decoration: InputDecoration( filled: true, - labelText: replying ? widget.quotable!.sender : '', - labelStyle: TextStyle(color: Theme.of(context).colorScheme.secondary), - contentPadding: Dimensions.inputContentPadding.copyWith(right: 36), + labelText: + replying ? widget.quotable!.sender : '', + labelStyle: TextStyle( + color: + Theme.of(context).colorScheme.secondary), + contentPadding: Dimensions.inputContentPadding + .copyWith(right: 36), focusedBorder: OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).colorScheme.secondary, + color: + Theme.of(context).colorScheme.secondary, width: 1, ), borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), - bottomLeft: Radius.circular(!replying ? 24 : 0), - bottomRight: Radius.circular(!replying ? 24 : 0), + bottomLeft: + Radius.circular(!replying ? 24 : 0), + bottomRight: + Radius.circular(!replying ? 24 : 0), ), ), border: OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).colorScheme.secondary, + color: + Theme.of(context).colorScheme.secondary, width: 1, ), borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), - bottomLeft: Radius.circular(!replying ? 24 : 0), - bottomRight: Radius.circular(!replying ? 24 : 0), + bottomLeft: + Radius.circular(!replying ? 24 : 0), + bottomRight: + Radius.circular(!replying ? 24 : 0), ), ), ), @@ -503,7 +516,7 @@ class ChatInputState extends State { ), //////// TEXT FIELD //////// Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -539,13 +552,16 @@ class ChatInputState extends State { enableSuggestions: props.suggestionsEnabled, textCapitalization: props.textCapitalization, keyboardType: TextInputType.multiline, - textInputAction: - widget.enterSend ? TextInputAction.send : TextInputAction.newline, + textInputAction: widget.enterSend + ? TextInputAction.send + : TextInputAction.newline, cursorColor: props.inputCursorColor, focusNode: widget.focusNode, controller: widget.controller, - onChanged: (text) => onUpdate(text, props: props), - onSubmitted: !isSendable ? null : (text) => onSubmit(), + onChanged: (text) => + onUpdate(text, props: props), + onSubmitted: + !isSendable ? null : (text) => onSubmit(), style: TextStyle( height: 1.5, color: props.inputTextColor, @@ -565,30 +581,45 @@ class ChatInputState extends State { ), ), fillColor: props.inputColorBackground, - contentPadding: Dimensions.inputContentPadding, + contentPadding: + Dimensions.inputContentPadding, focusedBorder: OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context) + .colorScheme + .secondary, width: 1, ), borderRadius: BorderRadius.only( - topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - topRight: - Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), - bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), + topLeft: Radius.circular(!replying + ? DEFAULT_BORDER_RADIUS + : 0), + topRight: Radius.circular(!replying + ? DEFAULT_BORDER_RADIUS + : 0), + bottomLeft: Radius.circular( + DEFAULT_BORDER_RADIUS), + bottomRight: Radius.circular( + DEFAULT_BORDER_RADIUS), )), border: OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context) + .colorScheme + .secondary, width: 1, ), borderRadius: BorderRadius.only( - topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - topRight: - Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), - bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), - bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), + topLeft: Radius.circular(!replying + ? DEFAULT_BORDER_RADIUS + : 0), + topRight: Radius.circular(!replying + ? DEFAULT_BORDER_RADIUS + : 0), + bottomLeft: Radius.circular( + DEFAULT_BORDER_RADIUS), + bottomRight: Radius.circular( + DEFAULT_BORDER_RADIUS), )), ), ), @@ -623,7 +654,8 @@ class ChatInputState extends State { Container( constraints: BoxConstraints( maxWidth: width, - maxHeight: imageHeight, // HACK: figure out why it overflows on Nexus 5x + maxHeight: + imageHeight, // HACK: figure out why it overflows on Nexus 5x ), child: ListLocalImages( imageSize: imageWidth, @@ -694,18 +726,17 @@ class _Props extends Equatable { final Function onSendTyping; - const _Props({ - required this.room, - required this.inputTextColor, - required this.inputCursorColor, - required this.inputColorBackground, - required this.enterSendEnabled, - required this.autocorrectEnabled, - required this.suggestionsEnabled, - required this.textCapitalization, - required this.onSendTyping, - required this.users - }); + const _Props( + {required this.room, + required this.inputTextColor, + required this.inputCursorColor, + required this.inputColorBackground, + required this.enterSendEnabled, + required this.autocorrectEnabled, + required this.suggestionsEnabled, + required this.textCapitalization, + required this.onSendTyping, + required this.users}); @override List get props => [ @@ -716,14 +747,18 @@ class _Props extends Equatable { static _Props mapStateToProps(Store store, String roomId) => _Props( room: selectRoom(id: roomId, state: store.state), users: roomUsers(store.state, roomId), - inputTextColor: selectInputTextColor(store.state.settingsStore.themeSettings.themeType), - inputCursorColor: selectCursorColor(store.state.settingsStore.themeSettings.themeType), - inputColorBackground: - selectInputBackgroundColor(store.state.settingsStore.themeSettings.themeType), + inputTextColor: selectInputTextColor( + store.state.settingsStore.themeSettings.themeType), + inputCursorColor: selectCursorColor( + store.state.settingsStore.themeSettings.themeType), + inputColorBackground: selectInputBackgroundColor( + store.state.settingsStore.themeSettings.themeType), enterSendEnabled: store.state.settingsStore.enterSendEnabled, autocorrectEnabled: store.state.settingsStore.autocorrectEnabled, suggestionsEnabled: store.state.settingsStore.suggestionsEnabled, - textCapitalization: Platform.isIOS ? TextCapitalization.sentences : TextCapitalization.none, + textCapitalization: Platform.isIOS + ? TextCapitalization.sentences + : TextCapitalization.none, onSendTyping: ({typing, roomId}) => store.dispatch( sendTyping(typing: typing, roomId: roomId), ), diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 926236444..d09fe3fa0 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -5,17 +5,15 @@ import 'package:syphon/store/user/model.dart'; import 'package:syphon/store/user/selectors.dart'; import 'package:syphon/views/widgets/avatars/avatar.dart'; - -class Mention extends StatefulWidget{ - - Mention({ - Key? key, - required this.users, - required this.width, - required this.controller, - this.visible = false, - this.height = 200 - }) : super(key: key); +class Mention extends StatefulWidget { + Mention( + {Key? key, + required this.users, + required this.width, + required this.controller, + this.visible = false, + this.height = 200}) + : super(key: key); final List users; final double width; @@ -23,50 +21,47 @@ class Mention extends StatefulWidget{ final TextEditingController controller; bool visible; - @override State createState() => MentionState(); } -class MentionState extends State{ - - +class MentionState extends State { @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: widget.height, - maxWidth: widget.width - ), - child: ListView.builder(itemBuilder: (buildContext, index){ - final String userName = formatUsername(widget.users[index] as User); - return Visibility( - visible: widget.visible, - child: Card( - child: ListTile( - onTap: () => onTab(widget.users[index]), - leading: Avatar( - uri: widget.users[index]?.avatarUri, - alt: userName, - size: Dimensions.avatarSizeMin, - background: AppColors.hashedColor( - userName, - ),), - title: Text(userName), - subtitle: Text(widget.users[index]?.userId), - ), - )); - }, - itemCount: widget.users.length, - shrinkWrap: true, - scrollDirection: Axis.vertical, - physics: ClampingScrollPhysics(), - padding: EdgeInsets.symmetric(vertical: 5), - )); + constraints: + BoxConstraints(maxHeight: widget.height, maxWidth: widget.width), + child: ListView.builder( + itemBuilder: (buildContext, index) { + final String userName = formatUsername(widget.users[index] as User); + return Visibility( + visible: widget.visible, + child: Card( + child: ListTile( + onTap: () => onTab(widget.users[index]), + leading: Avatar( + uri: widget.users[index]?.avatarUri, + alt: userName, + size: Dimensions.avatarSizeMin, + background: AppColors.hashedColor( + userName, + ), + ), + title: Text(userName), + subtitle: Text(widget.users[index]?.userId), + ), + )); + }, + itemCount: widget.users.length, + shrinkWrap: true, + scrollDirection: Axis.vertical, + physics: ClampingScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 5), + )); } - onTab(User user){ - setState((){ + onTab(User user) { + setState(() { widget.controller.text = '${widget.controller.text} ${user.userId}'; widget.controller.selection = TextSelection.fromPosition( TextPosition(offset: widget.controller.text.length)); @@ -74,4 +69,3 @@ class MentionState extends State{ }); } } - From 3cb1cc0de7ce145ad9a3f53fe4abfcb9b12e17d1 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 20 Aug 2022 23:42:51 +0200 Subject: [PATCH 28/42] fixed mention bug duplicate @ --- lib/views/home/chat/widgets/chat-mention.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index d09fe3fa0..619ef2b98 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -62,7 +62,7 @@ class MentionState extends State { onTab(User user) { setState(() { - widget.controller.text = '${widget.controller.text} ${user.userId}'; + widget.controller.text = '${widget.controller.text.replaceAll("@", "")} ${user.userId}'; // replace duplicate @ with empty string widget.controller.selection = TextSelection.fromPosition( TextPosition(offset: widget.controller.text.length)); widget.visible = false; From abfb2e6548431ca9d2c4529c7ba3232fba414405 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 20 Aug 2022 23:59:11 +0200 Subject: [PATCH 29/42] fixed bug --- lib/views/home/chat/widgets/chat-mention.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 619ef2b98..37f3fdb6f 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -62,7 +62,11 @@ class MentionState extends State { onTab(User user) { setState(() { - widget.controller.text = '${widget.controller.text.replaceAll("@", "")} ${user.userId}'; // replace duplicate @ with empty string + final textLength = widget.controller.text.length; + + widget.controller.text = '${widget.controller.text.substring(0, + textLength - 1)} ${user.userId}'; // avoid duplicate @ + widget.controller.selection = TextSelection.fromPosition( TextPosition(offset: widget.controller.text.length)); widget.visible = false; From 2c20ce19a49e47e2f637cf27e89862ef0dd1baec Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 3 Sep 2022 13:53:54 +0200 Subject: [PATCH 30/42] updated mention logic --- lib/views/home/chat/widgets/chat-input.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 70fdd34d8..8d3975b15 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -150,16 +150,24 @@ class ChatInputState extends State { // mention-dialog setState(() { - mention = false; + final int cursorPos = widget.controller.selection.baseOffset; users = props!.users; - if (text.startsWith('@') || text.contains(' @')) { - mention = true; + if(cursorPos > 0) { + if(text[cursorPos - 1] == '@' && !mention) { + mention = true; + } + else if (text[cursorPos - 1] == ' ') { + mention = false; + } + } + + if (mention) { users = users.where((user) { if (user != null) { final String searchText = text .toLowerCase() - .substring(text.indexOf('@') + 1, text.length); + .substring(text.lastIndexOf('@') + 1, text.length); if (user.userId != null) { return user.userId!.toLowerCase().contains(searchText); From b81508745c28cf38f14f39ff1724743ba6a78d2e Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 3 Sep 2022 14:02:32 +0200 Subject: [PATCH 31/42] updated setState --- lib/views/home/chat/widgets/chat-input.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 8d3975b15..45fc43fed 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -149,17 +149,18 @@ class ChatInputState extends State { }); // mention-dialog - setState(() { - final int cursorPos = widget.controller.selection.baseOffset; - users = props!.users; + final int cursorPos = widget.controller.selection.baseOffset; + users = props!.users; if(cursorPos > 0) { - if(text[cursorPos - 1] == '@' && !mention) { - mention = true; - } - else if (text[cursorPos - 1] == ' ') { - mention = false; - } + setState((){ + if(text[cursorPos - 1] == '@' && !mention) { + mention = true; + } + else if (text[cursorPos - 1] == ' ') { + mention = false; + } + }); } if (mention) { @@ -181,7 +182,6 @@ class ChatInputState extends State { } else { users = []; } - }); // start an interval for updating typing status if (widget.focusNode.hasFocus && typingNotifier == null) { From b31679d113a7f8cb580d8a84f8aef0e6e6545014 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 3 Sep 2022 14:07:05 +0200 Subject: [PATCH 32/42] code Style updated --- lib/views/home/chat/widgets/chat-input.dart | 3 +-- lib/views/home/chat/widgets/chat-mention.dart | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 45fc43fed..68e4ed166 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -23,12 +23,11 @@ import 'package:syphon/store/rooms/selectors.dart'; import 'package:syphon/store/settings/theme-settings/selectors.dart'; import 'package:syphon/store/user/model.dart'; import 'package:syphon/store/user/selectors.dart'; +import 'package:syphon/views/home/chat/widgets/chat-mention.dart'; import 'package:syphon/views/widgets/buttons/button-text.dart'; import 'package:syphon/views/widgets/containers/media-card.dart'; import 'package:syphon/views/widgets/lists/list-local-images.dart'; -import 'chat-mention.dart'; - const DEFAULT_BORDER_RADIUS = 24.0; _empty({ diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 37f3fdb6f..ce1436ac1 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -29,8 +29,10 @@ class MentionState extends State { @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: - BoxConstraints(maxHeight: widget.height, maxWidth: widget.width), + constraints: BoxConstraints( + maxHeight: widget.height, + maxWidth: widget.width + ), child: ListView.builder( itemBuilder: (buildContext, index) { final String userName = formatUsername(widget.users[index] as User); From dd094f7845ec6fc3a13cbe0fe799ca559a585b46 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 3 Sep 2022 23:52:54 +0200 Subject: [PATCH 33/42] updated state management --- lib/views/home/chat/widgets/chat-input.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 68e4ed166..12b16930f 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -152,14 +152,16 @@ class ChatInputState extends State { users = props!.users; if(cursorPos > 0) { - setState((){ if(text[cursorPos - 1] == '@' && !mention) { - mention = true; + setState((){ + mention = true; + }); } else if (text[cursorPos - 1] == ' ') { - mention = false; + setState((){ + mention = false; + }); } - }); } if (mention) { From b68066b957999a967fc03389019aa043fc237228 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 4 Sep 2022 14:27:08 +0200 Subject: [PATCH 34/42] fixed bug --- lib/views/home/chat/widgets/chat-mention.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index ce1436ac1..61654c4fd 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -65,9 +65,9 @@ class MentionState extends State { onTab(User user) { setState(() { final textLength = widget.controller.text.length; + final text = widget.controller.text; - widget.controller.text = '${widget.controller.text.substring(0, - textLength - 1)} ${user.userId}'; // avoid duplicate @ + widget.controller.text = '${text.substring(0, text.lastIndexOf("@"))} ${user.userId}'; widget.controller.selection = TextSelection.fromPosition( TextPosition(offset: widget.controller.text.length)); From 60683565331a3c85f8d7926dde0f4fbff2d7b3e6 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 4 Sep 2022 22:00:33 +0200 Subject: [PATCH 35/42] replaced mention logic with regex --- lib/views/home/chat/widgets/chat-input.dart | 55 +++++++++------------ 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 12b16930f..d59ec1911 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -148,41 +148,32 @@ class ChatInputState extends State { }); // mention-dialog - final int cursorPos = widget.controller.selection.baseOffset; users = props!.users; - if(cursorPos > 0) { - if(text[cursorPos - 1] == '@' && !mention) { - setState((){ - mention = true; - }); - } - else if (text[cursorPos - 1] == ' ') { - setState((){ - mention = false; - }); - } - } + final cursorPos = widget.controller.selection.baseOffset; + final subString = text.substring(0, cursorPos); + + mention = false; + + final RegExp mentionExpEnd = RegExp( + r'\B@\w+$', + caseSensitive: false, + multiLine: false, + ); + + final match = mentionExpEnd.firstMatch(subString); + + if(match != null){ + final mention = subString.substring(match.start, match.end); + final mentionName = mention.substring(1, mention.length); + + users = users.where((user) => user!.displayName!.toLowerCase().contains(mentionName.toLowerCase())).toList(); + + setState(() { + this.mention = users.isNotEmpty; + }); + } - if (mention) { - users = users.where((user) { - if (user != null) { - final String searchText = text - .toLowerCase() - .substring(text.lastIndexOf('@') + 1, text.length); - - if (user.userId != null) { - return user.userId!.toLowerCase().contains(searchText); - } else { - return user.displayName!.toLowerCase().contains(searchText); - } - } else { - return false; - } - }).toList(); - } else { - users = []; - } // start an interval for updating typing status if (widget.focusNode.hasFocus && typingNotifier == null) { From 8e23ec10027f50a50d652586181d77e80964f133 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 4 Sep 2022 22:01:24 +0200 Subject: [PATCH 36/42] code review --- lib/views/home/chat/widgets/chat-input.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index d59ec1911..936bb50b9 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -149,12 +149,11 @@ class ChatInputState extends State { // mention-dialog users = props!.users; + mention = false; final cursorPos = widget.controller.selection.baseOffset; final subString = text.substring(0, cursorPos); - mention = false; - final RegExp mentionExpEnd = RegExp( r'\B@\w+$', caseSensitive: false, From 2f778dac40999823e036a52eee9cb7c9c11c1320 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 4 Sep 2022 22:33:05 +0200 Subject: [PATCH 37/42] fixed bug --- lib/views/home/chat/widgets/chat-mention.dart | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 61654c4fd..18705e85e 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -63,15 +63,23 @@ class MentionState extends State { } onTab(User user) { - setState(() { - final textLength = widget.controller.text.length; final text = widget.controller.text; + final cursorPos = widget.controller.selection.baseOffset; - widget.controller.text = '${text.substring(0, text.lastIndexOf("@"))} ${user.userId}'; + final subText = text.substring(0, cursorPos); + final RegExp mentionExpEnd = RegExp( + r'\B@\w+$', + caseSensitive: false, + multiLine: false, + ); + + widget.controller.text = subText.replaceAll(mentionExpEnd, '${user.userId} ') + text.substring(cursorPos); widget.controller.selection = TextSelection.fromPosition( - TextPosition(offset: widget.controller.text.length)); - widget.visible = false; - }); + TextPosition(offset: widget.controller.text.length)); // move cursor to end + + setState(() { + widget.visible = false; + }); } } From 60ee6588fbb8e33c4de1438f8dc036451792c820 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sun, 4 Sep 2022 22:37:28 +0200 Subject: [PATCH 38/42] fixed code style --- lib/views/home/chat/widgets/chat-mention.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 18705e85e..639ad5aaa 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -65,7 +65,6 @@ class MentionState extends State { onTab(User user) { final text = widget.controller.text; final cursorPos = widget.controller.selection.baseOffset; - final subText = text.substring(0, cursorPos); final RegExp mentionExpEnd = RegExp( From 32635796b9f9edbd21a775d82f4cb564e95ca307 Mon Sep 17 00:00:00 2001 From: Florian Sabonchi Date: Sat, 10 Sep 2022 18:47:53 +0200 Subject: [PATCH 39/42] fixed bug if user was null --- lib/views/home/chat/widgets/chat-input.dart | 8 ++++++-- lib/views/home/chat/widgets/chat-mention.dart | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 936bb50b9..299ee99cf 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -166,7 +167,10 @@ class ChatInputState extends State { final mention = subString.substring(match.start, match.end); final mentionName = mention.substring(1, mention.length); - users = users.where((user) => user!.displayName!.toLowerCase().contains(mentionName.toLowerCase())).toList(); + users = props.users.where((user) { + return user!.displayName!.toLowerCase().contains(mentionName.toLowerCase()); + }).toList(); + setState(() { this.mention = users.isNotEmpty; @@ -425,7 +429,7 @@ class ChatInputState extends State { width: MediaQuery.of(context).size.width - Dimensions.buttonSendSize * 2.1, // HACK: fix the width of the dialog - users: users, + users: users.whereNotNull().toList(), controller: widget.controller, )), Visibility( diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 639ad5aaa..6446047ec 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -15,7 +15,7 @@ class Mention extends StatefulWidget { this.height = 200}) : super(key: key); - final List users; + final List users; final double width; final double height; final TextEditingController controller; @@ -35,14 +35,14 @@ class MentionState extends State { ), child: ListView.builder( itemBuilder: (buildContext, index) { - final String userName = formatUsername(widget.users[index] as User); + final String userName = formatUsername(widget.users[index]); return Visibility( visible: widget.visible, child: Card( child: ListTile( onTap: () => onTab(widget.users[index]), leading: Avatar( - uri: widget.users[index]?.avatarUri, + uri: widget.users[index].avatarUri, alt: userName, size: Dimensions.avatarSizeMin, background: AppColors.hashedColor( @@ -50,7 +50,7 @@ class MentionState extends State { ), ), title: Text(userName), - subtitle: Text(widget.users[index]?.userId), + subtitle: Text(widget.users[index].userId!), ), )); }, From 23e54d9da93b4732a630ce7be7d3a9e7517c5997 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sat, 10 Sep 2022 22:06:41 +0100 Subject: [PATCH 40/42] add user null check --- lib/views/home/chat/widgets/chat-input.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 299ee99cf..14cacf53a 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -168,7 +168,10 @@ class ChatInputState extends State { final mentionName = mention.substring(1, mention.length); users = props.users.where((user) { - return user!.displayName!.toLowerCase().contains(mentionName.toLowerCase()); + return user != null && + user!.displayName! + .toLowerCase() + .contains(mentionName.toLowerCase()); }).toList(); From 701ccc4c4462abf24b1b01467a0888f58d08bf5b Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Sat, 10 Sep 2022 22:06:50 +0100 Subject: [PATCH 41/42] linter --- lib/views/home/chat/widgets/chat-input.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index 14cacf53a..a42bd2d8d 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -163,7 +163,7 @@ class ChatInputState extends State { final match = mentionExpEnd.firstMatch(subString); - if(match != null){ + if (match != null) { final mention = subString.substring(match.start, match.end); final mentionName = mention.substring(1, mention.length); @@ -174,13 +174,11 @@ class ChatInputState extends State { .contains(mentionName.toLowerCase()); }).toList(); - setState(() { this.mention = users.isNotEmpty; }); } - // start an interval for updating typing status if (widget.focusNode.hasFocus && typingNotifier == null) { props!.onSendTyping(typing: true, roomId: props.room.id); From 22ae58c932ed34b8f951955cbd6764b0356fff55 Mon Sep 17 00:00:00 2001 From: ereio Date: Sun, 4 Dec 2022 17:25:03 -0500 Subject: [PATCH 42/42] refactor: cleaned up mentions branch using chore/upgrades-and-cleanup, bugs addressed after refactor --- lib/store/user/selectors.dart | 10 +- lib/views/home/chat/widgets/chat-input.dart | 202 +++++++++--------- lib/views/home/chat/widgets/chat-mention.dart | 125 ++++++----- 3 files changed, 163 insertions(+), 174 deletions(-) diff --git a/lib/store/user/selectors.dart b/lib/store/user/selectors.dart index 10a82c391..90c659a3d 100644 --- a/lib/store/user/selectors.dart +++ b/lib/store/user/selectors.dart @@ -35,9 +35,12 @@ List selectKnownUsers(AppState state) { return List.from(latestUsers); } -List roomUsers(AppState state, String? roomId) { +List roomUsers(AppState state, String? roomId) { final room = state.roomStore.rooms[roomId!] ?? Room(id: roomId); - return room.userIds.map((userId) => state.userStore.users[userId]).toList(); + return List.from(room.userIds + .map((userId) => state.userStore.users[userId]) + .where((userValid) => userValid != null) + .toList()); } Map messageUsers({required AppState state, String? roomId}) { @@ -89,7 +92,6 @@ List searchUsersLocal( } return List.from(users.where( - (user) => - (user!.displayName ?? '').contains(searchText) || (user.userId ?? '').contains(searchText), + (user) => (user!.displayName ?? '').contains(searchText) || (user.userId ?? '').contains(searchText), )); } diff --git a/lib/views/home/chat/widgets/chat-input.dart b/lib/views/home/chat/widgets/chat-input.dart index a42bd2d8d..3024dce22 100644 --- a/lib/views/home/chat/widgets/chat-input.dart +++ b/lib/views/home/chat/widgets/chat-input.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'dart:io'; -import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/svg.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:redux/redux.dart'; import 'package:syphon/global/algos.dart'; import 'package:syphon/global/assets.dart'; @@ -83,11 +83,9 @@ class ChatInput extends StatefulWidget { class ChatInputState extends State { ChatInputState() : super(); - bool mention = false; - List users = []; - bool sendable = false; bool showAttachments = false; + List mentioned = []; double keyboardHeight = 0; @@ -143,39 +141,37 @@ class ChatInputState extends State { } } - onUpdate(String text, {_Props? props}) { + onUpdateInputText(String text, {_Props? props}) { setState(() { sendable = text.trim().isNotEmpty; }); - // mention-dialog - users = props!.users; - mention = false; - - final cursorPos = widget.controller.selection.baseOffset; - final subString = text.substring(0, cursorPos); + final cursorPosition = widget.controller.selection.baseOffset; + final cursorTextPrior = text.substring(0, cursorPosition); - final RegExp mentionExpEnd = RegExp( - r'\B@\w+$', + final RegExp mentionExpanded = RegExp( + r'\B@\w+$', // match all text after an @ symbol but before the end of the line / space caseSensitive: false, multiLine: false, ); - final match = mentionExpEnd.firstMatch(subString); + final match = mentionExpanded.firstMatch(cursorTextPrior); + // find matches for mentions if (match != null) { - final mention = subString.substring(match.start, match.end); + final mention = cursorTextPrior.substring(match.start, match.end); final mentionName = mention.substring(1, mention.length); - users = props.users.where((user) { - return user != null && - user!.displayName! - .toLowerCase() - .contains(mentionName.toLowerCase()); + final usersMentioned = (props?.users ?? []).where((user) { + return user.displayName!.toLowerCase().contains(mentionName.toLowerCase()); }).toList(); setState(() { - this.mention = users.isNotEmpty; + mentioned = usersMentioned; + }); + } else { + setState(() { + mentioned = []; }); } @@ -210,6 +206,7 @@ class ChatInputState extends State { }); }); } + widget.onUpdateMessage?.call(text); } @@ -267,6 +264,34 @@ class ChatInputState extends State { onToggleMediaOptions(); } + showDialogForPhotoPermission(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text( + Strings.titleDialogPhotoPermission, + style: TextStyle(fontWeight: FontWeight.w600), + ), + content: Text(Strings.contentPhotoPermission), + actions: [ + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + }, + child: Text(Strings.buttonCancel), + ), + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + openAppSettings(); + }, + child: Text(Strings.buttonNext), + ), + ], + ), + ); + } + onAddFile() async { final pickerResult = await FilePicker.platform.pickFiles( type: FileType.any, @@ -282,8 +307,7 @@ class ChatInputState extends State { @override Widget build(BuildContext context) => StoreConnector( distinct: true, - converter: (Store store) => - _Props.mapStateToProps(store, widget.roomId), + converter: (Store store) => _Props.mapStateToProps(store, widget.roomId), onInitialBuild: onMounted, builder: (context, props) { final double width = MediaQuery.of(context).size.width; @@ -292,24 +316,18 @@ class ChatInputState extends State { // dynamic dimensions final double messageInputWidth = width - 72; - final bool replying = - widget.quotable != null && widget.quotable!.sender != null; + final bool replying = widget.quotable != null && widget.quotable!.sender != null; final bool loading = widget.sending; - final double maxInputHeight = - replying ? height * 0.45 : height * 0.65; - final double maxMediaHeight = - keyboardHeight > 0 ? keyboardHeight : height * 0.38; + final double maxInputHeight = replying ? height * 0.45 : height * 0.65; + final double maxMediaHeight = keyboardHeight > 0 ? keyboardHeight : height * 0.38; - final imageHeight = keyboardHeight > 0 - ? maxMediaHeight * 0.65 - : imageWidth; // 2 images in view + final imageHeight = keyboardHeight > 0 ? maxMediaHeight * 0.65 : imageWidth; // 2 images in view final isSendable = (sendable && !widget.sending) || // account for if editing - widget.editing && - (widget.editorController?.text.isNotEmpty ?? false); + widget.editing && (widget.editorController?.text.isNotEmpty ?? false); - final bool showMention = mention; + final bool showMention = mentioned.isNotEmpty; Color sendButtonColor = const Color(AppColors.blueBubbly); @@ -425,13 +443,13 @@ class ChatInputState extends State { //////// MENTION DIALOG //////// Container( padding: EdgeInsets.symmetric(horizontal: 15, vertical: 5), - child: Mention( + child: MentionDialog( visible: showMention, width: MediaQuery.of(context).size.width - - Dimensions.buttonSendSize * - 2.1, // HACK: fix the width of the dialog - users: users.whereNotNull().toList(), + Dimensions.buttonSendSize * 2.1, // HACK: fix the width of the dialog + users: mentioned, controller: widget.controller, + onComplete: () => onUpdateInputText(widget.controller.text, props: props), )), Visibility( visible: replying, @@ -460,41 +478,31 @@ class ChatInputState extends State { ), decoration: InputDecoration( filled: true, - labelText: - replying ? widget.quotable!.sender : '', - labelStyle: TextStyle( - color: - Theme.of(context).colorScheme.secondary), - contentPadding: Dimensions.inputContentPadding - .copyWith(right: 36), + labelText: replying ? widget.quotable!.sender : '', + labelStyle: TextStyle(color: Theme.of(context).colorScheme.secondary), + contentPadding: Dimensions.inputContentPadding.copyWith(right: 36), focusedBorder: OutlineInputBorder( borderSide: BorderSide( - color: - Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.secondary, width: 1, ), borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), - bottomLeft: - Radius.circular(!replying ? 24 : 0), - bottomRight: - Radius.circular(!replying ? 24 : 0), + bottomLeft: Radius.circular(!replying ? 24 : 0), + bottomRight: Radius.circular(!replying ? 24 : 0), ), ), border: OutlineInputBorder( borderSide: BorderSide( - color: - Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.secondary, width: 1, ), borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), - bottomLeft: - Radius.circular(!replying ? 24 : 0), - bottomRight: - Radius.circular(!replying ? 24 : 0), + bottomLeft: Radius.circular(!replying ? 24 : 0), + bottomRight: Radius.circular(!replying ? 24 : 0), ), ), ), @@ -556,16 +564,13 @@ class ChatInputState extends State { enableSuggestions: props.suggestionsEnabled, textCapitalization: props.textCapitalization, keyboardType: TextInputType.multiline, - textInputAction: widget.enterSend - ? TextInputAction.send - : TextInputAction.newline, + textInputAction: + widget.enterSend ? TextInputAction.send : TextInputAction.newline, cursorColor: props.inputCursorColor, focusNode: widget.focusNode, controller: widget.controller, - onChanged: (text) => - onUpdate(text, props: props), - onSubmitted: - !isSendable ? null : (text) => onSubmit(), + onChanged: (text) => onUpdateInputText(text, props: props), + onSubmitted: !isSendable ? null : (text) => onSubmit(), style: TextStyle( height: 1.5, color: props.inputTextColor, @@ -585,45 +590,28 @@ class ChatInputState extends State { ), ), fillColor: props.inputColorBackground, - contentPadding: - Dimensions.inputContentPadding, + contentPadding: Dimensions.inputContentPadding, focusedBorder: OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context) - .colorScheme - .secondary, + color: Theme.of(context).colorScheme.secondary, width: 1, ), borderRadius: BorderRadius.only( - topLeft: Radius.circular(!replying - ? DEFAULT_BORDER_RADIUS - : 0), - topRight: Radius.circular(!replying - ? DEFAULT_BORDER_RADIUS - : 0), - bottomLeft: Radius.circular( - DEFAULT_BORDER_RADIUS), - bottomRight: Radius.circular( - DEFAULT_BORDER_RADIUS), + topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), + topRight: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), + bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), + bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), )), border: OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context) - .colorScheme - .secondary, + color: Theme.of(context).colorScheme.secondary, width: 1, ), borderRadius: BorderRadius.only( - topLeft: Radius.circular(!replying - ? DEFAULT_BORDER_RADIUS - : 0), - topRight: Radius.circular(!replying - ? DEFAULT_BORDER_RADIUS - : 0), - bottomLeft: Radius.circular( - DEFAULT_BORDER_RADIUS), - bottomRight: Radius.circular( - DEFAULT_BORDER_RADIUS), + topLeft: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), + topRight: Radius.circular(!replying ? DEFAULT_BORDER_RADIUS : 0), + bottomLeft: Radius.circular(DEFAULT_BORDER_RADIUS), + bottomRight: Radius.circular(DEFAULT_BORDER_RADIUS), )), ), ), @@ -658,8 +646,7 @@ class ChatInputState extends State { Container( constraints: BoxConstraints( maxWidth: width, - maxHeight: - imageHeight, // HACK: figure out why it overflows on Nexus 5x + maxHeight: imageHeight, // HACK: figure out why it overflows on Nexus 5x ), child: ListLocalImages( imageSize: imageWidth, @@ -679,7 +666,15 @@ class ChatInputState extends State { child: MediaCard( text: Strings.buttonGallery, icon: Icons.photo, - onPress: () => onAddPhoto(), + onPress: () async { + const photosPermission = Permission.photos; + final status = await photosPermission.status; + if (!status.isGranted) { + showDialogForPhotoPermission(context); + } else { + onAddPhoto(); + } + }, ), ), Padding( @@ -726,7 +721,7 @@ class _Props extends Equatable { final bool autocorrectEnabled; final bool suggestionsEnabled; final TextCapitalization textCapitalization; - final List users; + final List users; final Function onSendTyping; @@ -751,18 +746,13 @@ class _Props extends Equatable { static _Props mapStateToProps(Store store, String roomId) => _Props( room: selectRoom(id: roomId, state: store.state), users: roomUsers(store.state, roomId), - inputTextColor: selectInputTextColor( - store.state.settingsStore.themeSettings.themeType), - inputCursorColor: selectCursorColor( - store.state.settingsStore.themeSettings.themeType), - inputColorBackground: selectInputBackgroundColor( - store.state.settingsStore.themeSettings.themeType), + inputTextColor: selectInputTextColor(store.state.settingsStore.themeSettings.themeType), + inputCursorColor: selectCursorColor(store.state.settingsStore.themeSettings.themeType), + inputColorBackground: selectInputBackgroundColor(store.state.settingsStore.themeSettings.themeType), enterSendEnabled: store.state.settingsStore.enterSendEnabled, autocorrectEnabled: store.state.settingsStore.autocorrectEnabled, suggestionsEnabled: store.state.settingsStore.suggestionsEnabled, - textCapitalization: Platform.isIOS - ? TextCapitalization.sentences - : TextCapitalization.none, + textCapitalization: Platform.isIOS ? TextCapitalization.sentences : TextCapitalization.none, onSendTyping: ({typing, roomId}) => store.dispatch( sendTyping(typing: typing, roomId: roomId), ), diff --git a/lib/views/home/chat/widgets/chat-mention.dart b/lib/views/home/chat/widgets/chat-mention.dart index 6446047ec..f7ea4a857 100644 --- a/lib/views/home/chat/widgets/chat-mention.dart +++ b/lib/views/home/chat/widgets/chat-mention.dart @@ -1,84 +1,81 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:syphon/global/colors.dart'; import 'package:syphon/global/dimensions.dart'; import 'package:syphon/store/user/model.dart'; import 'package:syphon/store/user/selectors.dart'; import 'package:syphon/views/widgets/avatars/avatar.dart'; -class Mention extends StatefulWidget { - Mention( - {Key? key, - required this.users, - required this.width, - required this.controller, - this.visible = false, - this.height = 200}) - : super(key: key); +class MentionDialog extends HookWidget implements PreferredSizeWidget { + const MentionDialog({ + required this.users, + required this.width, + required this.controller, + required this.onComplete, + this.visible = false, + this.height = 200, + }); + + @override + Size get preferredSize => AppBar().preferredSize; final List users; final double width; final double height; final TextEditingController controller; - bool visible; + final bool visible; - @override - State createState() => MentionState(); -} - -class MentionState extends State { - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: widget.height, - maxWidth: widget.width - ), - child: ListView.builder( - itemBuilder: (buildContext, index) { - final String userName = formatUsername(widget.users[index]); - return Visibility( - visible: widget.visible, - child: Card( - child: ListTile( - onTap: () => onTab(widget.users[index]), - leading: Avatar( - uri: widget.users[index].avatarUri, - alt: userName, - size: Dimensions.avatarSizeMin, - background: AppColors.hashedColor( - userName, - ), - ), - title: Text(userName), - subtitle: Text(widget.users[index].userId!), - ), - )); - }, - itemCount: widget.users.length, - shrinkWrap: true, - scrollDirection: Axis.vertical, - physics: ClampingScrollPhysics(), - padding: EdgeInsets.symmetric(vertical: 5), - )); - } + final Function onComplete; onTab(User user) { - final text = widget.controller.text; - final cursorPos = widget.controller.selection.baseOffset; - final subText = text.substring(0, cursorPos); + final text = controller.text; + final cursorPos = controller.selection.baseOffset; + final subText = text.substring(0, cursorPos); + + final RegExp mentionExpEnd = RegExp( + r'\B@\w+$', + caseSensitive: false, + multiLine: false, + ); - final RegExp mentionExpEnd = RegExp( - r'\B@\w+$', - caseSensitive: false, - multiLine: false, - ); + controller.text = subText.replaceAll(mentionExpEnd, '${user.userId} ') + text.substring(cursorPos); - widget.controller.text = subText.replaceAll(mentionExpEnd, '${user.userId} ') + text.substring(cursorPos); - widget.controller.selection = TextSelection.fromPosition( - TextPosition(offset: widget.controller.text.length)); // move cursor to end + controller.selection = TextSelection.fromPosition( + TextPosition(offset: controller.text.length), // move cursor to end + ); - setState(() { - widget.visible = false; - }); + onComplete(); } + + @override + Widget build(BuildContext context) => ConstrainedBox( + constraints: BoxConstraints(maxHeight: height, maxWidth: width), + child: ListView.builder( + itemBuilder: (buildContext, index) { + final String userName = formatUsername(users[index]); + + return Visibility( + visible: visible, + child: Card( + child: ListTile( + onTap: () => onTab(users[index]), + leading: Avatar( + uri: users[index].avatarUri, + alt: userName, + size: Dimensions.avatarSizeMin, + background: AppColors.hashedColor( + userName, + ), + ), + title: Text(userName), + subtitle: Text(users[index].userId!), + ), + )); + }, + itemCount: users.length, + shrinkWrap: true, + scrollDirection: Axis.vertical, + physics: ClampingScrollPhysics(), + padding: EdgeInsets.symmetric(vertical: 5), + )); }