diff --git a/assets/chat.svg b/assets/chat.svg new file mode 100644 index 0000000..b80f20d --- /dev/null +++ b/assets/chat.svg @@ -0,0 +1,185 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart index 8758bd6..6f8bc69 100644 --- a/lib/components/list_items/message.dart +++ b/lib/components/list_items/message.dart @@ -156,12 +156,14 @@ class Message extends StatelessWidget { color: selected ? Theme.of(context).primaryColor.withAlpha(100) : Theme.of(context).primaryColor.withAlpha(0), - padding: EdgeInsets.only( - left: 8.0, right: 8.0, bottom: sameSender ? 4.0 : 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: rowMainAxisAlignment, - children: rowChildren, + child: Padding( + padding: EdgeInsets.only( + left: 8.0, right: 8.0, bottom: sameSender ? 4.0 : 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: rowMainAxisAlignment, + children: rowChildren, + ), ), ), ); diff --git a/lib/main.dart b/lib/main.dart index cc31647..f5e20a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,6 +48,7 @@ class App extends StatelessWidget { textTheme: TextTheme( title: TextStyle( color: Colors.black, + fontSize: 20, ), ), iconTheme: IconThemeData(color: Colors.black), diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 7e53b8f..d0a7635 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -17,6 +17,7 @@ import 'package:fluffychat/views/chat_encryption_settings.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:toast/toast.dart'; import 'package:pedantic/pedantic.dart'; @@ -75,6 +76,16 @@ class _ChatState extends State<_Chat> { bool get selectMode => selectedEvents.isNotEmpty; + bool _loadingHistory = false; + + final int _loadHistoryCount = 100; + + void requestHistory() async { + setState(() => this._loadingHistory = true); + await timeline.requestHistory(historyCount: _loadHistoryCount); + setState(() => this._loadingHistory = false); + } + @override void initState() { _scrollController.addListener(() async { @@ -83,7 +94,7 @@ class _ChatState extends State<_Chat> { timeline.events.isNotEmpty && timeline.events[timeline.events.length - 1].type != EventTypes.RoomCreate) { - await timeline.requestHistory(historyCount: 100); + requestHistory(); } if (_scrollController.position.pixels > 0 && showScrollDownButton == false) { @@ -132,6 +143,9 @@ class _ChatState extends State<_Chat> { if (timeline.events.isNotEmpty) { unawaited(room.sendReadReceipt(timeline.events.first.eventId)); } + if (timeline.events.length < _loadHistoryCount) { + this.requestHistory(); + } } updateView(); return true; @@ -330,13 +344,14 @@ class _ChatState extends State<_Chat> { ? Container() : Icon(Icons.edit, color: Theme.of(context).primaryColor, - size: 10), + size: 13), SizedBox(width: 4), Text( typingText, style: TextStyle( color: Theme.of(context).primaryColor, fontStyle: FontStyle.italic, + fontSize: 16, ), ), ], @@ -372,282 +387,325 @@ class _ChatState extends State<_Chat> { ), ) : null, - body: SafeArea( - child: Column( - children: [ - Expanded( - child: FutureBuilder( - future: getTimeline(), - builder: (BuildContext context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - if (room.notificationCount != null && - room.notificationCount > 0 && - timeline != null && - timeline.events.isNotEmpty) { - room.sendReadReceipt(timeline.events.first.eventId); - } - - if (timeline.events.isEmpty) return Container(); - - return ListView.builder( - reverse: true, - itemCount: timeline.events.length + 1, - controller: _scrollController, - itemBuilder: (BuildContext context, int i) { - return i == 0 - ? AnimatedContainer( - height: seenByText.isEmpty ? 0 : 24, - duration: seenByText.isEmpty - ? Duration(milliseconds: 0) - : Duration(milliseconds: 500), - alignment: timeline.events.first.senderId == - client.userID - ? Alignment.topRight - : Alignment.topLeft, - child: Text( - seenByText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).primaryColor, - ), - ), - padding: EdgeInsets.only( - left: 8, - right: 8, - bottom: 8, - ), - ) - : Message(timeline.events[i - 1], - onSelect: (Event event) => event.redacted - ? null - : selectedEvents.contains(event) - ? setState( - () => selectedEvents.remove(event)) - : setState( - () => selectedEvents.add(event)), - longPressSelect: selectedEvents.isEmpty, - selected: selectedEvents - .contains(timeline.events[i - 1]), - timeline: timeline, - nextEvent: - i >= 2 ? timeline.events[i - 2] : null); - }); - }, - ), + body: Stack( + children: [ + if (!kIsWeb) + SvgPicture.asset( + "assets/chat.svg", + height: double.infinity, + color: Theme.of(context).primaryColor.withOpacity(0.2), ), - AnimatedContainer( - duration: Duration(milliseconds: 300), - height: replyEvent != null ? 56 : 0, - child: Material( - color: Theme.of(context).secondaryHeaderColor, - child: Row( - children: [ - IconButton( - icon: Icon(Icons.close), - onPressed: () => setState(() => replyEvent = null), + SafeArea( + child: Column( + children: [ + Material( + elevation: 1, + color: Theme.of(context).scaffoldBackgroundColor, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + height: _loadingHistory ? 40 : 0, + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + SizedBox(width: 8), + Text(I18n.of(context).loadingPleaseWait), + ], + ), ), - Expanded( - child: ReplyContent(replyEvent), - ), - ], + ), ), - ), - ), - room.canSendDefaultMessages && room.membership == Membership.join - ? Container( - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - spreadRadius: 1, - blurRadius: 2, - offset: Offset(0, -1), // changes position of shadow + Expanded( + child: FutureBuilder( + future: getTimeline(), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + + if (room.notificationCount != null && + room.notificationCount > 0 && + timeline != null && + timeline.events.isNotEmpty) { + room.sendReadReceipt(timeline.events.first.eventId); + } + + if (timeline.events.isEmpty) return Container(); + + return ListView.builder( + reverse: true, + itemCount: timeline.events.length + 1, + controller: _scrollController, + itemBuilder: (BuildContext context, int i) { + return i == 0 + ? AnimatedContainer( + height: seenByText.isEmpty ? 0 : 24, + duration: seenByText.isEmpty + ? Duration(milliseconds: 0) + : Duration(milliseconds: 500), + alignment: timeline.events.first.senderId == + client.userID + ? Alignment.topRight + : Alignment.topLeft, + child: Text( + seenByText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).primaryColor, + ), + ), + padding: EdgeInsets.only( + left: 8, + right: 8, + bottom: 8, + ), + ) + : Message(timeline.events[i - 1], + onSelect: (Event event) => event.redacted + ? null + : selectedEvents.contains(event) + ? setState(() => + selectedEvents.remove(event)) + : setState(() => + selectedEvents.add(event)), + longPressSelect: selectedEvents.isEmpty, + selected: selectedEvents + .contains(timeline.events[i - 1]), + timeline: timeline, + nextEvent: + i >= 2 ? timeline.events[i - 2] : null); + }); + }, + ), + ), + AnimatedContainer( + duration: Duration(milliseconds: 300), + height: replyEvent != null ? 56 : 0, + child: Material( + color: Theme.of(context).secondaryHeaderColor, + child: Row( + children: [ + IconButton( + icon: Icon(Icons.close), + onPressed: () => setState(() => replyEvent = null), + ), + Expanded( + child: ReplyContent(replyEvent), ), ], ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: selectMode - ? [ - Container( - height: 56, - child: FlatButton( - onPressed: () => forwardEventsAction(context), - child: Row( - children: [ - Icon(Icons.keyboard_arrow_left), - Text(I18n.of(context).forward), - ], - ), - ), - ), - selectedEvents.length == 1 - ? selectedEvents.first.status > 0 - ? Container( - height: 56, - child: FlatButton( - onPressed: () => replyAction(), - child: Row( - children: [ - Text(I18n.of(context).reply), - Icon( - Icons.keyboard_arrow_right), - ], - ), - ), - ) - : Container( - height: 56, - child: FlatButton( - onPressed: () => sendAgainAction(), - child: Row( - children: [ - Text(I18n.of(context) - .tryToSendAgain), - SizedBox(width: 4), - Icon(Icons.send, size: 16), - ], - ), - ), - ) - : Container(), - ] - : [ - kIsWeb - ? Container() - : PopupMenuButton( - icon: Icon(Icons.add), - onSelected: (String choice) async { - if (choice == "file") { - sendFileAction(context); - } else if (choice == "image") { - sendImageAction(context); - } - if (choice == "camera") { - openCameraAction(context); - } - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: "file", - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - child: Icon(Icons.attachment), - ), - title: - Text(I18n.of(context).sendFile), - contentPadding: EdgeInsets.all(0), - ), - ), - PopupMenuItem( - value: "image", - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - child: Icon(Icons.image), - ), - title: Text( - I18n.of(context).sendImage), - contentPadding: EdgeInsets.all(0), - ), - ), - PopupMenuItem( - value: "camera", - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.purple, - foregroundColor: Colors.white, - child: Icon(Icons.camera), - ), - title: Text( - I18n.of(context).openCamera), - contentPadding: EdgeInsets.all(0), - ), - ), - ], + ), + ), + room.canSendDefaultMessages && + room.membership == Membership.join + ? Container( + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 2, + offset: + Offset(0, -1), // changes position of shadow + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: selectMode + ? [ + Container( + height: 56, + child: FlatButton( + onPressed: () => + forwardEventsAction(context), + child: Row( + children: [ + Icon(Icons.keyboard_arrow_left), + Text(I18n.of(context).forward), + ], + ), ), - SizedBox(width: 8), - Expanded( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 4.0), - child: TextField( - minLines: 1, - maxLines: kIsWeb ? 1 : 8, - keyboardType: kIsWeb - ? TextInputType.text - : TextInputType.multiline, - onSubmitted: (String text) { - send(); - FocusScope.of(context) - .requestFocus(inputFocus); - }, - focusNode: inputFocus, - controller: sendController, - decoration: InputDecoration( - hintText: I18n.of(context).writeAMessage, - border: InputBorder.none, - suffixIcon: sendController.text.isEmpty - ? InkWell( - child: Icon(room.encrypted - ? Icons.lock - : Icons.lock_open), - onTap: () => - Navigator.of(context).push( - AppRoute.defaultRoute( - context, - ChatEncryptionSettingsView( - widget.id), + ), + selectedEvents.length == 1 + ? selectedEvents.first.status > 0 + ? Container( + height: 56, + child: FlatButton( + onPressed: () => replyAction(), + child: Row( + children: [ + Text( + I18n.of(context).reply), + Icon(Icons + .keyboard_arrow_right), + ], ), ), ) - : null, + : Container( + height: 56, + child: FlatButton( + onPressed: () => + sendAgainAction(), + child: Row( + children: [ + Text(I18n.of(context) + .tryToSendAgain), + SizedBox(width: 4), + Icon(Icons.send, size: 16), + ], + ), + ), + ) + : Container(), + ] + : [ + kIsWeb + ? Container() + : PopupMenuButton( + icon: Icon(Icons.add), + onSelected: (String choice) async { + if (choice == "file") { + sendFileAction(context); + } else if (choice == "image") { + sendImageAction(context); + } + if (choice == "camera") { + openCameraAction(context); + } + }, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: "file", + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + child: Icon(Icons.attachment), + ), + title: Text( + I18n.of(context).sendFile), + contentPadding: + EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: "image", + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + child: Icon(Icons.image), + ), + title: Text( + I18n.of(context).sendImage), + contentPadding: + EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: "camera", + child: ListTile( + leading: CircleAvatar( + backgroundColor: + Colors.purple, + foregroundColor: Colors.white, + child: Icon(Icons.camera), + ), + title: Text(I18n.of(context) + .openCamera), + contentPadding: + EdgeInsets.all(0), + ), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0), + child: TextField( + minLines: 1, + maxLines: kIsWeb ? 1 : 8, + keyboardType: kIsWeb + ? TextInputType.text + : TextInputType.multiline, + onSubmitted: (String text) { + send(); + FocusScope.of(context) + .requestFocus(inputFocus); + }, + focusNode: inputFocus, + controller: sendController, + decoration: InputDecoration( + hintText: + I18n.of(context).writeAMessage, + border: InputBorder.none, + prefixIcon: + sendController.text.isEmpty + ? InkWell( + child: Icon(room.encrypted + ? Icons.lock + : Icons.lock_open), + onTap: () => + Navigator.of(context) + .push( + AppRoute.defaultRoute( + context, + ChatEncryptionSettingsView( + widget.id), + ), + ), + ) + : null, + ), + onChanged: (String text) { + this.typingCoolDown?.cancel(); + this.typingCoolDown = + Timer(Duration(seconds: 2), () { + this.typingCoolDown = null; + this.currentlyTyping = false; + room.sendTypingInfo(false); + }); + this.typingTimeout ??= + Timer(Duration(seconds: 30), () { + this.typingTimeout = null; + this.currentlyTyping = false; + }); + if (!this.currentlyTyping) { + this.currentlyTyping = true; + room.sendTypingInfo(true, + timeout: Duration(seconds: 30) + .inMilliseconds); + } + }, + ), ), - onChanged: (String text) { - this.typingCoolDown?.cancel(); - this.typingCoolDown = - Timer(Duration(seconds: 2), () { - this.typingCoolDown = null; - this.currentlyTyping = false; - room.sendTypingInfo(false); - }); - this.typingTimeout ??= - Timer(Duration(seconds: 30), () { - this.typingTimeout = null; - this.currentlyTyping = false; - }); - if (!this.currentlyTyping) { - this.currentlyTyping = true; - room.sendTypingInfo(true, - timeout: Duration(seconds: 30) - .inMilliseconds); - } - }, ), - ), - ), - IconButton( - icon: Icon(Icons.send), - onPressed: () => send(), - ), - ], - ), - ) - : Container(), - ], - ), + IconButton( + icon: Icon(Icons.send), + onPressed: () => send(), + ), + ], + ), + ) + : Container(), + ], + ), + ), + ], ), ); } diff --git a/lib/views/new_private_chat.dart b/lib/views/new_private_chat.dart index 001616d..bcbbfb8 100644 --- a/lib/views/new_private_chat.dart +++ b/lib/views/new_private_chat.dart @@ -104,7 +104,6 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { @override Widget build(BuildContext context) { - final String defaultDomain = Matrix.of(context).client.userID.domain; return Scaffold( appBar: AppBar( title: Text(I18n.of(context).newPrivateChat), @@ -163,8 +162,7 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { ) : Icon(Icons.account_circle), prefixText: "@", - hintText: - "${I18n.of(context).username.toLowerCase()}:$defaultDomain", + hintText: "${I18n.of(context).username.toLowerCase()}", ), ), ), @@ -190,7 +188,7 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { ), title: Text( foundProfile["display_name"] ?? - foundProfile["user_id"].localpart, + (foundProfile["user_id"] as String).localpart, style: TextStyle(), maxLines: 1, ), diff --git a/pubspec.lock b/pubspec.lock index efa0bf8..e5a2ed2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,8 +117,8 @@ packages: dependency: "direct main" description: path: "." - ref: e2fde3fa924cb9a1bdccf5dd6b63aad8d820d20a - resolved-ref: e2fde3fa924cb9a1bdccf5dd6b63aad8d820d20a + ref: "0f68b60f16db924b10fa8954623e67de6252b35f" + resolved-ref: "0f68b60f16db924b10fa8954623e67de6252b35f" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" @@ -181,6 +181,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.5" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.1" flutter_test: dependency: "direct dev" description: flutter @@ -317,6 +324,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.4" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" path_provider: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 54d67d5..9eda8c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Chat with your friends. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.7.2+22 +version: 0.7.3+23 environment: sdk: ">=2.6.0 <3.0.0" @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: e2fde3fa924cb9a1bdccf5dd6b63aad8d820d20a + ref: 0f68b60f16db924b10fa8954623e67de6252b35f localstorage: ^3.0.1+4 bubble: ^1.1.9+1 @@ -49,6 +49,7 @@ dependencies: http: ^0.12.0+4 universal_html: ^1.1.12 uni_links: ^0.2.0 + flutter_svg: ^0.17.1 intl: ^0.16.0 intl_translation: ^0.17.9 @@ -84,6 +85,7 @@ flutter: - assets/logo.png - assets/private_chat_wallpaper.png - assets/new_group_wallpaper.png + - assets/chat.svg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see