Merge branch 'feature/new-ui' into feature/file-picker
@@ -8,7 +8,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<application
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
android:label="example"
|
android:label="ChatSample"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -1 +1 @@
|
|||||||
bdf1751976c4ee1e2ef7638eabcfd1ea
|
bb5f9103d9045cd6244bcae1f5f343e5
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 572 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 990 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 7.4 KiB |
@@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>example</string>
|
<string>ChatSample</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
@@ -98,7 +98,9 @@ class MyApp extends StatelessWidget {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
theme: ThemeData.light(),
|
theme: ThemeData.light(),
|
||||||
darkTheme: ThemeData.dark(),
|
darkTheme: ThemeData.dark(),
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.light,
|
||||||
|
|
||||||
|
///TODO change to system once dark theme is implemented
|
||||||
builder: (context, widget) {
|
builder: (context, widget) {
|
||||||
return StreamChat(
|
return StreamChat(
|
||||||
child: widget,
|
child: widget,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
name: example
|
name: example
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
version: 1.0.39+41
|
|
||||||
|
version: 1.0.43+45
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.2.2 <3.0.0"
|
sdk: ">=2.2.2 <3.0.0"
|
||||||
@@ -18,7 +19,13 @@ dev_dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_driver:
|
flutter_driver:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_launcher_icons: ^0.8.0
|
||||||
test: any
|
test: any
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
|
flutter_icons:
|
||||||
|
android: true
|
||||||
|
ios: true
|
||||||
|
image_path: "assets/icon.png"
|
||||||
|
|||||||
@@ -41,195 +41,211 @@ class GiphyAttachment extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Card(
|
DecoratedBox(
|
||||||
clipBehavior: Clip.antiAlias,
|
decoration: BoxDecoration(),
|
||||||
shape: RoundedRectangleBorder(
|
child: Card(
|
||||||
borderRadius: BorderRadius.only(
|
elevation: 2,
|
||||||
topRight: Radius.circular(16.0),
|
clipBehavior: Clip.antiAlias,
|
||||||
bottomRight: Radius.circular(0.0),
|
shape: RoundedRectangleBorder(
|
||||||
topLeft: Radius.circular(16.0),
|
borderRadius: BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(16.0),
|
topRight: Radius.circular(16.0),
|
||||||
|
bottomRight: Radius.circular(0.0),
|
||||||
|
topLeft: Radius.circular(16.0),
|
||||||
|
bottomLeft: Radius.circular(16.0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Column(
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
children: <Widget>[
|
||||||
children: <Widget>[
|
Stack(
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.all(8.0),
|
Padding(
|
||||||
child: GestureDetector(
|
padding: const EdgeInsets.all(8.0),
|
||||||
onTap: () {
|
child: GestureDetector(
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) {
|
onTap: () {
|
||||||
return FullScreenImage(
|
Navigator.push(context,
|
||||||
url: attachment.imageUrl ??
|
MaterialPageRoute(builder: (_) {
|
||||||
attachment.assetUrl ??
|
return FullScreenImage(
|
||||||
attachment.thumbUrl,
|
url: attachment.imageUrl ??
|
||||||
);
|
attachment.assetUrl ??
|
||||||
}));
|
attachment.thumbUrl,
|
||||||
},
|
);
|
||||||
child: CachedNetworkImage(
|
}));
|
||||||
height: size?.height,
|
},
|
||||||
width: size?.width,
|
child: CachedNetworkImage(
|
||||||
placeholder: (_, __) {
|
height: size?.height,
|
||||||
return Container(
|
width: size?.width,
|
||||||
width: size?.width,
|
placeholder: (_, __) {
|
||||||
height: size?.height,
|
return Container(
|
||||||
child: Center(
|
width: size?.width,
|
||||||
child: CircularProgressIndicator(),
|
height: size?.height,
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
imageUrl: attachment.thumbUrl ??
|
||||||
|
attachment.imageUrl ??
|
||||||
|
attachment.assetUrl,
|
||||||
|
errorWidget: (context, url, error) => AttachmentError(
|
||||||
|
attachment: attachment,
|
||||||
|
size: size,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
imageUrl: attachment.thumbUrl ??
|
|
||||||
attachment.imageUrl ??
|
|
||||||
attachment.assetUrl,
|
|
||||||
errorWidget: (context, url, error) => AttachmentError(
|
|
||||||
attachment: attachment,
|
|
||||||
size: size,
|
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
Positioned(
|
||||||
),
|
left: 0,
|
||||||
),
|
top: 0,
|
||||||
),
|
child: Container(
|
||||||
Positioned(
|
decoration: BoxDecoration(
|
||||||
left: 0,
|
color: Colors.white,
|
||||||
top: 0,
|
borderRadius: BorderRadius.only(
|
||||||
child: Container(
|
bottomRight: Radius.circular(16.0),
|
||||||
decoration: BoxDecoration(
|
)),
|
||||||
color: Colors.white,
|
child: Padding(
|
||||||
borderRadius: BorderRadius.only(
|
padding: const EdgeInsets.only(
|
||||||
bottomRight: Radius.circular(16.0),
|
left: 8.0,
|
||||||
)),
|
right: 8.0,
|
||||||
child: Padding(
|
top: 8.0,
|
||||||
padding: const EdgeInsets.only(
|
bottom: 4.0,
|
||||||
left: 8.0,
|
),
|
||||||
right: 8.0,
|
child: Row(
|
||||||
top: 8.0,
|
children: [
|
||||||
bottom: 4.0,
|
Icon(
|
||||||
),
|
StreamIcons.lightning,
|
||||||
child: Row(
|
color: StreamChatTheme.of(context).accentColor,
|
||||||
children: [
|
size: 16.0,
|
||||||
Icon(
|
),
|
||||||
StreamIcons.lightning,
|
Text(
|
||||||
color: StreamChatTheme.of(context).accentColor,
|
'GIPHY',
|
||||||
size: 16.0,
|
style: TextStyle(
|
||||||
),
|
color:
|
||||||
Text(
|
StreamChatTheme.of(context).accentColor,
|
||||||
'GIPHY',
|
fontWeight: FontWeight.bold,
|
||||||
style: TextStyle(
|
fontSize: 11.0,
|
||||||
color: StreamChatTheme.of(context).accentColor,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
fontSize: 11.0,
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
if (attachment.title != null)
|
||||||
if (attachment.title != null)
|
Container(
|
||||||
Container(
|
alignment: Alignment.bottomCenter,
|
||||||
alignment: Alignment.bottomCenter,
|
child: Padding(
|
||||||
child: Material(
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
color: Colors.white,
|
child: Row(
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Card(
|
||||||
Card(
|
elevation: 2,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(
|
padding: const EdgeInsets.all(0),
|
||||||
StreamIcons.left,
|
constraints: BoxConstraints.tight(Size(32, 32)),
|
||||||
size: 24.0,
|
icon: Icon(
|
||||||
|
StreamIcons.left,
|
||||||
|
size: 24.0,
|
||||||
|
),
|
||||||
|
splashRadius: 16,
|
||||||
|
onPressed: () {
|
||||||
|
streamChannel.channel.sendAction(message, {
|
||||||
|
'image_action': 'shuffle',
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
splashRadius: 24,
|
shape: CircleBorder(),
|
||||||
onPressed: () {
|
|
||||||
streamChannel.channel.sendAction(message, {
|
|
||||||
'image_action': 'shuffle',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
shape: CircleBorder(),
|
Expanded(
|
||||||
),
|
child: Center(
|
||||||
Expanded(
|
child: Text(
|
||||||
child: Center(
|
'"${attachment.title}"',
|
||||||
child: Text(
|
style: TextStyle(
|
||||||
'"${attachment.title}"',
|
fontStyle: FontStyle.italic,
|
||||||
style: TextStyle(
|
),
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Card(
|
||||||
Card(
|
elevation: 2,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(
|
padding: const EdgeInsets.all(0),
|
||||||
StreamIcons.right,
|
constraints: BoxConstraints.tight(Size(32, 32)),
|
||||||
size: 24.0,
|
icon: Icon(
|
||||||
|
StreamIcons.right,
|
||||||
|
size: 24.0,
|
||||||
|
),
|
||||||
|
splashRadius: 16,
|
||||||
|
onPressed: () {
|
||||||
|
streamChannel.channel.sendAction(message, {
|
||||||
|
'image_action': 'shuffle',
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
splashRadius: 24,
|
shape: CircleBorder(),
|
||||||
onPressed: () {
|
|
||||||
streamChannel.channel.sendAction(message, {
|
|
||||||
'image_action': 'shuffle',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
shape: CircleBorder(),
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 4.0,
|
||||||
),
|
),
|
||||||
SizedBox(
|
Container(
|
||||||
height: 4.0,
|
color: Colors.black.withOpacity(0.2),
|
||||||
),
|
width: double.infinity,
|
||||||
Container(
|
height: 0.5,
|
||||||
color: Colors.black.withOpacity(0.2),
|
),
|
||||||
width: double.infinity,
|
Row(
|
||||||
height: 0.5,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
Expanded(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: FlatButton(
|
||||||
children: [
|
height: 50,
|
||||||
Expanded(
|
onPressed: () {
|
||||||
child: FlatButton(
|
streamChannel.channel.sendAction(message, {
|
||||||
height: 50,
|
'image_action': 'cancel',
|
||||||
onPressed: () {
|
});
|
||||||
streamChannel.channel.sendAction(message, {
|
},
|
||||||
'image_action': 'cancel',
|
child: Text(
|
||||||
});
|
'Cancel',
|
||||||
},
|
style: TextStyle(
|
||||||
child: Text(
|
fontWeight: FontWeight.bold,
|
||||||
'Cancel',
|
color: Colors.black.withOpacity(0.5)),
|
||||||
style: TextStyle(
|
),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.black.withOpacity(0.5)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Container(
|
||||||
Container(
|
width: 0.5,
|
||||||
width: 0.5,
|
color: Colors.black.withOpacity(0.2),
|
||||||
color: Colors.black.withOpacity(0.2),
|
height: 50.0,
|
||||||
height: 50.0,
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: FlatButton(
|
||||||
child: FlatButton(
|
height: 50,
|
||||||
height: 50,
|
onPressed: () {
|
||||||
onPressed: () {
|
streamChannel.channel.sendAction(message, {
|
||||||
streamChannel.channel.sendAction(message, {
|
'image_action': 'send',
|
||||||
'image_action': 'send',
|
});
|
||||||
});
|
},
|
||||||
},
|
child: Text(
|
||||||
child: Text(
|
'Send',
|
||||||
'Send',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: StreamChatTheme.of(context).accentColor,
|
||||||
color: StreamChatTheme.of(context).accentColor,
|
fontWeight: FontWeight.bold),
|
||||||
fontWeight: FontWeight.bold),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -313,7 +329,10 @@ class GiphyAttachment extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(
|
||||||
|
top: 8.0,
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
@@ -14,6 +16,7 @@ import 'package:stream_chat/stream_chat.dart';
|
|||||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||||
|
import 'package:substring_highlight/substring_highlight.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
import 'stream_channel.dart';
|
import 'stream_channel.dart';
|
||||||
@@ -168,7 +171,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
bool _messageIsPresent = false;
|
bool _messageIsPresent = false;
|
||||||
bool _typingStarted = false;
|
bool _typingStarted = false;
|
||||||
bool _commandEnabled = false;
|
bool _commandEnabled = false;
|
||||||
OverlayEntry _commandsOverlay, _mentionsOverlay;
|
OverlayEntry _commandsOverlay, _mentionsOverlay, _emojiOverlay;
|
||||||
|
Iterable<String> _emojiNames;
|
||||||
|
|
||||||
Command _chosenCommand;
|
Command _chosenCommand;
|
||||||
bool _actionsShrunk = false;
|
bool _actionsShrunk = false;
|
||||||
@@ -227,22 +231,66 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDmCheckbox() {
|
Widget _buildDmCheckbox() {
|
||||||
return Row(
|
return Container(
|
||||||
children: [
|
height: 36,
|
||||||
Checkbox(
|
padding: const EdgeInsets.only(
|
||||||
value: _sendAsDm,
|
left: 12,
|
||||||
onChanged: (val) => setState(
|
bottom: 12,
|
||||||
() {
|
top: 8,
|
||||||
_sendAsDm = val;
|
),
|
||||||
},
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
border: _sendAsDm
|
||||||
|
? null
|
||||||
|
: Border.all(
|
||||||
|
color: Colors.black.withOpacity(.5),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Material(
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
color: _sendAsDm
|
||||||
|
? StreamChatTheme.of(context).accentColor
|
||||||
|
: Colors.white,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_sendAsDm = !_sendAsDm;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: AnimatedCrossFade(
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
reverseDuration: Duration(milliseconds: 300),
|
||||||
|
crossFadeState: _sendAsDm
|
||||||
|
? CrossFadeState.showFirst
|
||||||
|
: CrossFadeState.showSecond,
|
||||||
|
firstChild: Icon(
|
||||||
|
StreamIcons.check,
|
||||||
|
size: 16.0,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
secondChild: SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
activeColor: StreamChatTheme.of(context).accentColor,
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
Padding(
|
child: Text('Send also as direct message'),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
),
|
||||||
child: Text('Send also as direct message'),
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,42 +374,14 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
_commandsOverlay = null;
|
_commandsOverlay = null;
|
||||||
_mentionsOverlay?.remove();
|
_mentionsOverlay?.remove();
|
||||||
_mentionsOverlay = null;
|
_mentionsOverlay = null;
|
||||||
|
_emojiOverlay?.remove();
|
||||||
|
_emojiOverlay = null;
|
||||||
|
|
||||||
if (s.startsWith('/')) {
|
_checkCommands(s.trimLeft(), context);
|
||||||
var matchedCommandsList = StreamChannel.of(context)
|
|
||||||
.channel
|
|
||||||
.config
|
|
||||||
.commands
|
|
||||||
.where((element) => element.name == s.substring(1))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (matchedCommandsList.length == 1) {
|
_checkMentions(s, context);
|
||||||
_chosenCommand = matchedCommandsList[0];
|
|
||||||
textEditingController.clear();
|
|
||||||
_messageIsPresent = false;
|
|
||||||
setState(() {
|
|
||||||
_commandEnabled = true;
|
|
||||||
});
|
|
||||||
_commandsOverlay?.remove();
|
|
||||||
_commandsOverlay = null;
|
|
||||||
} else {
|
|
||||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
|
||||||
Overlay.of(context).insert(_commandsOverlay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textEditingController.selection.isCollapsed &&
|
_checkEmoji(s, context);
|
||||||
s.isNotEmpty &&
|
|
||||||
(s[textEditingController.selection.start - 1] == '@' ||
|
|
||||||
textEditingController.text
|
|
||||||
.substring(
|
|
||||||
0, textEditingController.selection.start)
|
|
||||||
.split(' ')
|
|
||||||
.last
|
|
||||||
.contains('@'))) {
|
|
||||||
_mentionsOverlay = _buildMentionsOverlayEntry();
|
|
||||||
Overlay.of(context).insert(_mentionsOverlay);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -427,8 +447,73 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _checkEmoji(String s, BuildContext context) {
|
||||||
|
if (textEditingController.selection.isCollapsed &&
|
||||||
|
(s.isNotEmpty && s[textEditingController.selection.start - 1] == ':' ||
|
||||||
|
textEditingController.text
|
||||||
|
.substring(
|
||||||
|
0,
|
||||||
|
textEditingController.selection.start,
|
||||||
|
)
|
||||||
|
.contains(':'))) {
|
||||||
|
final textToSelection = textEditingController.text
|
||||||
|
.substring(0, textEditingController.value.selection.start);
|
||||||
|
final splits = textToSelection.split(':');
|
||||||
|
final query = splits[splits.length - 2]?.toLowerCase();
|
||||||
|
final emoji = Emoji.byName(query);
|
||||||
|
|
||||||
|
if (textToSelection.endsWith(':') && emoji != null) {
|
||||||
|
_chooseEmoji(splits.sublist(0, splits.length - 1), emoji);
|
||||||
|
} else {
|
||||||
|
_emojiOverlay = _buildEmojiOverlay();
|
||||||
|
|
||||||
|
if (_emojiOverlay != null) {
|
||||||
|
Overlay.of(context).insert(_emojiOverlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkMentions(String s, BuildContext context) {
|
||||||
|
if (textEditingController.selection.isCollapsed &&
|
||||||
|
(s.isNotEmpty && s[textEditingController.selection.start - 1] == '@' ||
|
||||||
|
textEditingController.text
|
||||||
|
.substring(0, textEditingController.selection.start)
|
||||||
|
.split(' ')
|
||||||
|
.last
|
||||||
|
.contains('@'))) {
|
||||||
|
_mentionsOverlay = _buildMentionsOverlayEntry();
|
||||||
|
Overlay.of(context).insert(_mentionsOverlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkCommands(String s, BuildContext context) {
|
||||||
|
if (s.startsWith('/')) {
|
||||||
|
var matchedCommandsList = StreamChannel.of(context)
|
||||||
|
.channel
|
||||||
|
.config
|
||||||
|
.commands
|
||||||
|
.where((element) => element.name == s.substring(1))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (matchedCommandsList.length == 1) {
|
||||||
|
_chosenCommand = matchedCommandsList[0];
|
||||||
|
textEditingController.clear();
|
||||||
|
_messageIsPresent = false;
|
||||||
|
setState(() {
|
||||||
|
_commandEnabled = true;
|
||||||
|
});
|
||||||
|
_commandsOverlay.remove();
|
||||||
|
_commandsOverlay = null;
|
||||||
|
} else {
|
||||||
|
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||||
|
Overlay.of(context).insert(_commandsOverlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OverlayEntry _buildCommandsOverlayEntry() {
|
OverlayEntry _buildCommandsOverlayEntry() {
|
||||||
final text = textEditingController.text;
|
final text = textEditingController.text.trimLeft();
|
||||||
final commands = StreamChannel.of(context)
|
final commands = StreamChannel.of(context)
|
||||||
.channel
|
.channel
|
||||||
.config
|
.config
|
||||||
@@ -456,13 +541,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints.loose(Size.fromHeight(400)),
|
constraints: BoxConstraints.loose(Size.fromHeight(400)),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
// boxShadow: [
|
|
||||||
// BoxShadow(
|
|
||||||
// spreadRadius: -8,
|
|
||||||
// blurRadius: 5.0,
|
|
||||||
// offset: Offset(0, -4),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
color: StreamChatTheme.of(context).primaryColor,
|
color: StreamChatTheme.of(context).primaryColor,
|
||||||
borderRadius: BorderRadius.circular(8.0)),
|
borderRadius: BorderRadius.circular(8.0)),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@@ -480,25 +558,19 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: Icon(StreamIcons.lightning,
|
child: Icon(StreamIcons.lightning,
|
||||||
color: StreamChatTheme.of(context).accentColor),
|
color: StreamChatTheme.of(context).accentColor),
|
||||||
),
|
),
|
||||||
Text('Instant Commands')
|
Text(
|
||||||
|
'Instant Commands',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black.withOpacity(.5),
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...commands
|
...commands
|
||||||
.map(
|
.map(
|
||||||
(c) => ListTile(
|
(c) => ListTile(
|
||||||
leading: c.name == 'giphy'
|
leading: c.name == 'giphy' ? _buildGiphyIcon() : null,
|
||||||
? CircleAvatar(
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
child: Image.asset(
|
|
||||||
'images/giphy_icon.png',
|
|
||||||
package: 'stream_chat_flutter',
|
|
||||||
width: 16.0,
|
|
||||||
height: 16.0,
|
|
||||||
),
|
|
||||||
maxRadius: 12.0,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
title: Text.rich(
|
title: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '${c.name.capitalize()}',
|
text: '${c.name.capitalize()}',
|
||||||
@@ -641,12 +713,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
case 0:
|
case 0:
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
itemCount: _mediaData.item2.length,
|
itemCount: _mediaData.item2.length,
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate:
|
||||||
crossAxisCount: 3),
|
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
|
||||||
itemBuilder: (context, position) {
|
itemBuilder: (context, position) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding:
|
||||||
horizontal: 1.0, vertical: 1.0),
|
const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -655,13 +727,13 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: Image.memory(
|
child: Image.memory(
|
||||||
_mediaData.item2[position],
|
_mediaData.item2[position],
|
||||||
fit: _mediaData.item1[position].height >
|
fit: _mediaData.item1[position].height >
|
||||||
_mediaData.item1[position].width
|
_mediaData.item1[position].width
|
||||||
? BoxFit.fitWidth
|
? BoxFit.fitWidth
|
||||||
: BoxFit.fitHeight,
|
: BoxFit.fitHeight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_attachments.any((element) =>
|
if (_attachments.any((element) =>
|
||||||
element.id == _mediaData.item1[position].id))
|
element.id == _mediaData.item1[position].id))
|
||||||
Container(
|
Container(
|
||||||
color: Colors.black.withOpacity(0.5),
|
color: Colors.black.withOpacity(0.5),
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -676,11 +748,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (!_attachments.any((element) =>
|
if (!_attachments.any((element) =>
|
||||||
element.id == _mediaData.item1[position].id)) {
|
element.id == _mediaData.item1[position].id)) {
|
||||||
_addAttachment(_mediaData.item1[position]);
|
_addAttachment(_mediaData.item1[position]);
|
||||||
} else {
|
} else {
|
||||||
_attachments.removeWhere((element) =>
|
_attachments.removeWhere((element) =>
|
||||||
element.id == _mediaData.item1[position].id);
|
element.id == _mediaData.item1[position].id);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -775,10 +847,36 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_mediaData = Tuple2<List<Medium>, List<dynamic>>(resultList, resultThumbnailList);
|
_mediaData =
|
||||||
|
Tuple2<List<Medium>, List<dynamic>>(resultList, resultThumbnailList);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CircleAvatar _buildGiphyIcon() {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return CircleAvatar(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
child: Image.asset(
|
||||||
|
'images/giphy_icon.png',
|
||||||
|
package: 'stream_chat_flutter',
|
||||||
|
width: 24.0,
|
||||||
|
height: 24.0,
|
||||||
|
),
|
||||||
|
radius: 12,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return CircleAvatar(
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'svgs/giphy_icon.svg',
|
||||||
|
package: 'stream_chat_flutter',
|
||||||
|
width: 24.0,
|
||||||
|
height: 24.0,
|
||||||
|
),
|
||||||
|
radius: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OverlayEntry _buildMentionsOverlayEntry() {
|
OverlayEntry _buildMentionsOverlayEntry() {
|
||||||
final splits = textEditingController.text
|
final splits = textEditingController.text
|
||||||
.substring(0, textEditingController.value.selection.start)
|
.substring(0, textEditingController.value.selection.start)
|
||||||
@@ -819,13 +917,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints.loose(Size.fromHeight(400)),
|
constraints: BoxConstraints.loose(Size.fromHeight(400)),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
spreadRadius: -8,
|
|
||||||
blurRadius: 5.0,
|
|
||||||
offset: Offset(0, -4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
color: StreamChatTheme.of(context).primaryColor,
|
color: StreamChatTheme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
child: FutureBuilder<List<Member>>(
|
child: FutureBuilder<List<Member>>(
|
||||||
@@ -838,6 +929,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
children: snapshot.data
|
children: snapshot.data
|
||||||
.map((m) => ListTile(
|
.map((m) => ListTile(
|
||||||
leading: UserAvatar(
|
leading: UserAvatar(
|
||||||
|
constraints: BoxConstraints.tight(
|
||||||
|
Size(
|
||||||
|
40,
|
||||||
|
40,
|
||||||
|
),
|
||||||
|
),
|
||||||
user: m.user,
|
user: m.user,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -878,6 +975,125 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OverlayEntry _buildEmojiOverlay() {
|
||||||
|
final splits = textEditingController.text
|
||||||
|
.substring(0, textEditingController.value.selection.start)
|
||||||
|
.split(':');
|
||||||
|
final query = splits.last.toLowerCase();
|
||||||
|
|
||||||
|
if (query.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final emojis = _emojiNames
|
||||||
|
.where((e) => e.contains(query))
|
||||||
|
.map((e) => Emoji.byName(e))
|
||||||
|
.where((e) => e != null);
|
||||||
|
|
||||||
|
if (emojis.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderBox renderBox = context.findRenderObject();
|
||||||
|
final size = renderBox.size;
|
||||||
|
|
||||||
|
return OverlayEntry(builder: (context) {
|
||||||
|
return Positioned(
|
||||||
|
bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.all(8.0),
|
||||||
|
elevation: 2.0,
|
||||||
|
color: StreamChatTheme.of(context).primaryColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints.loose(Size.fromHeight(200)),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
spreadRadius: -8,
|
||||||
|
blurRadius: 5.0,
|
||||||
|
offset: Offset(0, -4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
color: StreamChatTheme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: emojis.length + 1,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
if (i == 0) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0, top: 8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Icon(
|
||||||
|
StreamIcons.smile,
|
||||||
|
color: StreamChatTheme.of(context).accentColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'Emoji matching "$query"',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black.withOpacity(.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final emoji = emojis.elementAt(i - 1);
|
||||||
|
return ListTile(
|
||||||
|
title: SubstringHighlight(
|
||||||
|
text: "${emoji.char} ${emoji.name.replaceAll('_', ' ')}",
|
||||||
|
term: query,
|
||||||
|
textStyleHighlight:
|
||||||
|
Theme.of(context).textTheme.headline6.copyWith(
|
||||||
|
fontSize: 14.5,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
textStyle: Theme.of(context).textTheme.headline6.copyWith(
|
||||||
|
fontSize: 14.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_chooseEmoji(splits, emoji);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _chooseEmoji(List<String> splits, Emoji emoji) {
|
||||||
|
final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char;
|
||||||
|
|
||||||
|
textEditingController.value = TextEditingValue(
|
||||||
|
text: rejoin +
|
||||||
|
textEditingController.text
|
||||||
|
.substring(textEditingController.selection.start),
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: rejoin.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_emojiOverlay?.remove();
|
||||||
|
_emojiOverlay = null;
|
||||||
|
}
|
||||||
|
|
||||||
void _setCommand(Command c) {
|
void _setCommand(Command c) {
|
||||||
textEditingController.clear();
|
textEditingController.clear();
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -1433,11 +1649,13 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_emojiNames = Emoji.all().map((e) => e.name);
|
||||||
|
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
|
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
if (_commandsOverlay != null) {
|
if (_commandsOverlay != null) {
|
||||||
if (textEditingController.text.startsWith('/')) {
|
if (textEditingController.text.trimLeft().startsWith('/')) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||||
Overlay.of(context).insert(_commandsOverlay);
|
Overlay.of(context).insert(_commandsOverlay);
|
||||||
@@ -1453,6 +1671,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_emojiOverlay != null) {
|
||||||
|
if (textEditingController.text.contains(':')) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_emojiOverlay = _buildEmojiOverlay();
|
||||||
|
Overlay.of(context).insert(_emojiOverlay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_commandsOverlay != null) {
|
if (_commandsOverlay != null) {
|
||||||
_commandsOverlay.remove();
|
_commandsOverlay.remove();
|
||||||
@@ -1460,6 +1687,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
if (_mentionsOverlay != null) {
|
if (_mentionsOverlay != null) {
|
||||||
_mentionsOverlay.remove();
|
_mentionsOverlay.remove();
|
||||||
}
|
}
|
||||||
|
if (_emojiOverlay != null) {
|
||||||
|
_emojiOverlay.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1494,6 +1724,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_commandsOverlay?.remove();
|
_commandsOverlay?.remove();
|
||||||
|
_emojiOverlay?.remove();
|
||||||
_mentionsOverlay?.remove();
|
_mentionsOverlay?.remove();
|
||||||
_keyboardListener?.cancel();
|
_keyboardListener?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_widget.dart';
|
import 'package:stream_chat_flutter/src/message_widget.dart';
|
||||||
import 'package:stream_chat_flutter/src/system_message.dart';
|
import 'package:stream_chat_flutter/src/system_message.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
import 'package:widgets_visibility_provider/widgets_visibility_provider.dart';
|
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
import 'date_divider.dart';
|
import 'date_divider.dart';
|
||||||
@@ -106,7 +107,10 @@ class MessageListView extends StatefulWidget {
|
|||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
this.dateDividerBuilder,
|
this.dateDividerBuilder,
|
||||||
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
||||||
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
this.initialScrollIndex = 0,
|
||||||
|
this.initialAlignment = 0,
|
||||||
|
this.scrollController,
|
||||||
|
this.itemPositionListener,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Function used to build a custom message widget
|
/// Function used to build a custom message widget
|
||||||
@@ -131,95 +135,101 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// Builder used to render date dividers
|
/// Builder used to render date dividers
|
||||||
final Widget Function(DateTime) dateDividerBuilder;
|
final Widget Function(DateTime) dateDividerBuilder;
|
||||||
|
|
||||||
|
/// Index of an item to initially align within the viewport.
|
||||||
|
final int initialScrollIndex;
|
||||||
|
|
||||||
|
/// Determines where the leading edge of the item at [initialScrollIndex]
|
||||||
|
/// should be placed.
|
||||||
|
final double initialAlignment;
|
||||||
|
|
||||||
|
/// Controller for jumping or scrolling to an item.
|
||||||
|
final ItemScrollController scrollController;
|
||||||
|
|
||||||
|
/// Provides a listenable iterable of [itemPositions] of items that are on
|
||||||
|
/// screen and their locations.
|
||||||
|
final ItemPositionsListener itemPositionListener;
|
||||||
|
|
||||||
/// The ScrollPhysics used by the ListView
|
/// The ScrollPhysics used by the ListView
|
||||||
final ScrollPhysics scrollPhysics;
|
final ScrollPhysics scrollPhysics;
|
||||||
|
|
||||||
/// The [ScrollViewKeyboardDismissBehavior] used by the ListView
|
|
||||||
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageListViewState createState() => _MessageListViewState();
|
_MessageListViewState createState() => _MessageListViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MessageListViewState extends State<MessageListView> {
|
class _MessageListViewState extends State<MessageListView> {
|
||||||
static const _newMessageLoadingOffset = 100;
|
ItemScrollController _scrollController;
|
||||||
final ScrollController _scrollController = ScrollController();
|
bool _bottomWasVisible = false;
|
||||||
bool _bottomWasVisible = true;
|
|
||||||
bool _topWasVisible = false;
|
bool _topWasVisible = false;
|
||||||
List<Message> _messages = [];
|
|
||||||
List<Message> _newMessageList = [];
|
|
||||||
Function _onThreadTap;
|
Function _onThreadTap;
|
||||||
bool _showScrollToBottom = false;
|
bool _showScrollToBottom = false;
|
||||||
|
ItemPositionsListener _itemPositionListener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final streamChannel = StreamChannel.of(context);
|
final streamChannel = StreamChannel.of(context);
|
||||||
|
|
||||||
/// TODO: find a better solution when (https://github.com/flutter/flutter/issues/21023) is fixed
|
final messagesStream = widget.parentMessage != null
|
||||||
return WidgetsVisibilityProvider(
|
? streamChannel.channel.state.threadsStream
|
||||||
condition: (positionData) =>
|
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
||||||
positionData.endPosition >= 20 &&
|
.map((threads) => threads[widget.parentMessage.id])
|
||||||
positionData.startPosition <= positionData.viewportSize,
|
: streamChannel.channel.state.messagesStream;
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
return StreamBuilder<List<Message>>(
|
||||||
children: [
|
stream: messagesStream,
|
||||||
NotificationListener<ScrollNotification>(
|
initialData: widget.parentMessage != null
|
||||||
onNotification: (_) {
|
? streamChannel.channel.state.threads[widget.parentMessage.id]
|
||||||
if (_scrollController.offset < 150 &&
|
: streamChannel.channel.state.messages,
|
||||||
_newMessageList.isNotEmpty) {
|
builder: (context, snapshot) {
|
||||||
setState(() {
|
final messages = snapshot.data?.reversed?.toList() ?? [];
|
||||||
_messages.insertAll(0, _newMessageList);
|
return Stack(
|
||||||
_newMessageList.clear();
|
alignment: Alignment.center,
|
||||||
});
|
children: [
|
||||||
}
|
ScrollablePositionedList.builder(
|
||||||
return true;
|
itemPositionsListener: _itemPositionListener,
|
||||||
},
|
addAutomaticKeepAlives: true,
|
||||||
child: ListView.custom(
|
key: Key('messageListView'),
|
||||||
key: Key('messageListView'),
|
initialScrollIndex: widget.initialScrollIndex,
|
||||||
physics: widget.scrollPhysics,
|
initialAlignment: widget.initialAlignment,
|
||||||
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
physics: widget.scrollPhysics,
|
||||||
controller: _scrollController,
|
itemScrollController: _scrollController,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
childrenDelegate: SliverChildBuilderDelegate(
|
itemCount: messages.length +
|
||||||
(context, i) {
|
1 +
|
||||||
if (i == _messages.length + 1) {
|
(widget.parentMessage != null ? 1 : 0),
|
||||||
if (widget.parentMessage != null) {
|
itemBuilder: (context, i) {
|
||||||
if (widget.parentMessageBuilder != null) {
|
if (i == messages.length + 1) {
|
||||||
return widget.parentMessageBuilder(
|
if (widget.parentMessageBuilder != null) {
|
||||||
context,
|
return widget.parentMessageBuilder(
|
||||||
widget.parentMessage,
|
context,
|
||||||
);
|
widget.parentMessage,
|
||||||
} else {
|
);
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
buildParentMessage(widget.parentMessage),
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 32),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Text(
|
|
||||||
'Start of thread',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
color:
|
|
||||||
Theme.of(context).accentColor.withAlpha(50),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return SizedBox();
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
buildParentMessage(widget.parentMessage),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(
|
||||||
|
'Start of thread',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
color:
|
||||||
|
Theme.of(context).accentColor.withAlpha(50),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == _messages.length) {
|
if (i == messages.length) {
|
||||||
return _buildLoadingIndicator(streamChannel);
|
return _buildLoadingIndicator(streamChannel);
|
||||||
}
|
}
|
||||||
final message = _messages[i];
|
final message = messages[i];
|
||||||
final nextMessage = i > 0 ? _messages[i - 1] : null;
|
final nextMessage = i > 0 ? messages[i - 1] : null;
|
||||||
|
|
||||||
Widget messageWidget;
|
Widget messageWidget;
|
||||||
|
|
||||||
@@ -227,14 +237,14 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
messageWidget = _buildBottomMessage(
|
messageWidget = _buildBottomMessage(
|
||||||
context,
|
context,
|
||||||
message,
|
message,
|
||||||
_messages,
|
messages,
|
||||||
streamChannel,
|
streamChannel,
|
||||||
);
|
);
|
||||||
} else if (i == _messages.length - 1) {
|
} else if (i == messages.length - 1) {
|
||||||
messageWidget = _buildTopMessage(
|
messageWidget = _buildTopMessage(
|
||||||
context,
|
context,
|
||||||
message,
|
message,
|
||||||
_messages,
|
messages,
|
||||||
streamChannel,
|
streamChannel,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -246,13 +256,13 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
MessageDetails(
|
MessageDetails(
|
||||||
context,
|
context,
|
||||||
message,
|
message,
|
||||||
_messages,
|
messages,
|
||||||
i,
|
i,
|
||||||
),
|
),
|
||||||
_messages),
|
messages),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
messageWidget = buildMessage(message, _messages, i);
|
messageWidget = buildMessage(message, messages, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,84 +275,74 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
messageWidget,
|
messageWidget,
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
child: VisibleNotifierWidget(
|
child: widget.dateDividerBuilder != null
|
||||||
condition: (
|
? widget.dateDividerBuilder(
|
||||||
ScrollNotification previousNotification,
|
nextMessage.createdAt.toLocal())
|
||||||
PositionData previousPositionData,
|
: DateDivider(
|
||||||
ScrollNotification currentNotification,
|
dateTime: nextMessage.createdAt.toLocal(),
|
||||||
PositionData currentPositionData,
|
),
|
||||||
) {
|
|
||||||
print(
|
|
||||||
'currentPositionData.endPosition: ${currentPositionData.endPosition}');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
data: i,
|
|
||||||
child: widget.dateDividerBuilder != null
|
|
||||||
? widget.dateDividerBuilder(
|
|
||||||
nextMessage.createdAt.toLocal())
|
|
||||||
: DateDivider(
|
|
||||||
dateTime: nextMessage.createdAt.toLocal(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return VisibleNotifierWidget(
|
return messageWidget;
|
||||||
child: messageWidget,
|
|
||||||
data: i,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: _messages.length + 2,
|
|
||||||
findChildIndexCallback: (key) {
|
|
||||||
final ValueKey<String> valueKey = key;
|
|
||||||
final index = _messages
|
|
||||||
.indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value);
|
|
||||||
return index != -1 ? index : null;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
if (widget.showScrollToBottom)
|
||||||
),
|
StreamBuilder<int>(
|
||||||
if (widget.showScrollToBottom)
|
stream: streamChannel.channel.state.unreadCountStream,
|
||||||
StreamBuilder<Event>(
|
builder: (context, snapshot) {
|
||||||
stream: streamChannel.channel.on(
|
if (!_showScrollToBottom ||
|
||||||
EventType.messageNew,
|
!snapshot.hasData ||
|
||||||
),
|
snapshot.data == 0) {
|
||||||
builder: (context, _) {
|
return SizedBox();
|
||||||
if (!_showScrollToBottom ||
|
}
|
||||||
streamChannel.channel.state.unreadCount == 0) {
|
return _buildScrollToBottom(snapshot.data);
|
||||||
return SizedBox();
|
}),
|
||||||
}
|
Positioned(
|
||||||
return _buildScrollToBottom(streamChannel);
|
top: 20.0,
|
||||||
}),
|
child: ValueListenableBuilder<Iterable<ItemPosition>>(
|
||||||
Positioned(
|
valueListenable: _itemPositionListener.itemPositions,
|
||||||
top: 20.0,
|
builder: (context, values, _) {
|
||||||
child: WidgetsVisibilityConsumer(
|
final items = _itemPositionListener.itemPositions?.value;
|
||||||
listener: (
|
if (items.isEmpty || messages.isEmpty) {
|
||||||
context,
|
return SizedBox();
|
||||||
event,
|
}
|
||||||
) {},
|
|
||||||
builder: (context, event) {
|
|
||||||
if (event.positionDataList == null ||
|
|
||||||
event.positionDataList.isEmpty) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateDivider(
|
var index = _getTopElement(values).index;
|
||||||
dateTime: _messages[event.positionDataList.last.data]
|
|
||||||
.createdAt
|
if (index > messages.length) {
|
||||||
.toLocal(),
|
return SizedBox();
|
||||||
);
|
}
|
||||||
},
|
|
||||||
),
|
if (index == messages.length) {
|
||||||
),
|
index = max(index - 1, 0);
|
||||||
],
|
}
|
||||||
),
|
|
||||||
);
|
return widget.dateDividerBuilder != null
|
||||||
|
? widget.dateDividerBuilder(
|
||||||
|
messages[index].createdAt.toLocal(),
|
||||||
|
)
|
||||||
|
: DateDivider(
|
||||||
|
dateTime: messages[index].createdAt.toLocal(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScrollToBottom(StreamChannelState streamChannel) {
|
ItemPosition _getTopElement(Iterable<ItemPosition> values) {
|
||||||
|
return values
|
||||||
|
.where((ItemPosition position) => position.itemLeadingEdge < 0.9)
|
||||||
|
.reduce((ItemPosition max, ItemPosition position) =>
|
||||||
|
position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScrollToBottom(int unreadCount) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
@@ -358,13 +358,11 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Future.delayed(Duration(milliseconds: 500), () {
|
setState(() {
|
||||||
setState(() {
|
_showScrollToBottom = false;
|
||||||
_showScrollToBottom = false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
_scrollController.animateTo(
|
_scrollController.scrollTo(
|
||||||
0,
|
index: 0,
|
||||||
duration: Duration(seconds: 1),
|
duration: Duration(seconds: 1),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
@@ -377,7 +375,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
top: -10,
|
top: -10,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 20,
|
radius: 20,
|
||||||
child: Text(streamChannel.channel.state.unreadCount.toString()),
|
child: Text(unreadCount.toString()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -387,7 +385,9 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
Container _buildLoadingIndicator(StreamChannelState streamChannel) {
|
Container _buildLoadingIndicator(StreamChannelState streamChannel) {
|
||||||
return Container(
|
return Container(
|
||||||
|
key: Key('LOADING-INDICATOR'),
|
||||||
height: 50,
|
height: 50,
|
||||||
|
width: double.infinity,
|
||||||
child: StreamBuilder<bool>(
|
child: StreamBuilder<bool>(
|
||||||
stream: streamChannel.queryMessage,
|
stream: streamChannel.queryMessage,
|
||||||
initialData: false,
|
initialData: false,
|
||||||
@@ -401,7 +401,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!snapshot.data) {
|
if (!snapshot.data) {
|
||||||
return Container();
|
return SizedBox();
|
||||||
}
|
}
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -428,14 +428,14 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
MessageDetails(
|
MessageDetails(
|
||||||
context,
|
context,
|
||||||
message,
|
message,
|
||||||
_messages,
|
messages,
|
||||||
_messages.length - 1,
|
messages.length - 1,
|
||||||
),
|
),
|
||||||
_messages,
|
messages,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
messageWidget = buildMessage(message, messages, _messages.length - 1);
|
messageWidget = buildMessage(message, messages, messages.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return VisibilityDetector(
|
return VisibilityDetector(
|
||||||
@@ -470,10 +470,10 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
MessageDetails(
|
MessageDetails(
|
||||||
context,
|
context,
|
||||||
message,
|
message,
|
||||||
_messages,
|
messages,
|
||||||
0,
|
0,
|
||||||
),
|
),
|
||||||
_messages,
|
messages,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -492,9 +492,11 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_bottomWasVisible = isVisible;
|
_bottomWasVisible = isVisible;
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_showScrollToBottom = !isVisible;
|
setState(() {
|
||||||
});
|
_showScrollToBottom = !isVisible;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: messageWidget,
|
child: messageWidget,
|
||||||
);
|
);
|
||||||
@@ -541,6 +543,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
) {
|
) {
|
||||||
if (message.type == 'system' && message.text?.isNotEmpty == true) {
|
if (message.type == 'system' && message.text?.isNotEmpty == true) {
|
||||||
return SystemMessage(
|
return SystemMessage(
|
||||||
|
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||||
message: message,
|
message: message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -563,6 +566,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||||
|
|
||||||
return MessageWidget(
|
return MessageWidget(
|
||||||
|
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||||
message: message,
|
message: message,
|
||||||
reverse: isMyMessage,
|
reverse: isMyMessage,
|
||||||
showReactions: !message.isDeleted,
|
showReactions: !message.isDeleted,
|
||||||
@@ -600,64 +604,45 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription _streamListener;
|
StreamSubscription _messageNewListener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
_scrollController = widget.scrollController ?? ItemScrollController();
|
||||||
|
_itemPositionListener =
|
||||||
|
widget.itemPositionListener ?? ItemPositionsListener.create();
|
||||||
|
|
||||||
final streamChannel = StreamChannel.of(context);
|
final streamChannel = StreamChannel.of(context);
|
||||||
|
|
||||||
Stream<List<Message>> stream;
|
_messageNewListener =
|
||||||
|
streamChannel.channel.on(EventType.messageNew).listen((event) {
|
||||||
if (widget.parentMessage == null) {
|
final firstElementInViewport =
|
||||||
stream = streamChannel.channel.state.messagesStream;
|
_itemPositionListener.itemPositions.value.first;
|
||||||
} else {
|
if (event.message.user.id == streamChannel.channel.client.state.user.id) {
|
||||||
streamChannel.getReplies(widget.parentMessage.id);
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
stream = streamChannel.channel.state.threadsStream
|
_scrollController.jumpTo(
|
||||||
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
index: 0,
|
||||||
.map((threads) => threads[widget.parentMessage.id]);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
_streamListener = stream
|
|
||||||
.map((messages) =>
|
|
||||||
messages
|
|
||||||
?.where((m) =>
|
|
||||||
!(m.status == MessageSendingStatus.FAILED && m.isDeleted))
|
|
||||||
?.toList() ??
|
|
||||||
[])
|
|
||||||
.listen((newMessages) {
|
|
||||||
newMessages = newMessages.reversed.toList();
|
|
||||||
if (_messages.isEmpty ||
|
|
||||||
newMessages.isEmpty ||
|
|
||||||
newMessages.first.id != _messages.first.id) {
|
|
||||||
if (!_scrollController.hasClients ||
|
|
||||||
_scrollController.offset < _newMessageLoadingOffset) {
|
|
||||||
if (streamChannel.channel.state.unreadCount > 0) {
|
|
||||||
streamChannel.channel.markRead();
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_messages = newMessages;
|
|
||||||
});
|
|
||||||
} else if (newMessages.first.user.id ==
|
|
||||||
streamChannel.channel.client.state.user.id) {
|
|
||||||
_scrollController.jumpTo(0);
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
setState(() {
|
|
||||||
_messages = newMessages;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_newMessageList = newMessages;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_messages = newMessages;
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if (firstElementInViewport.index != 0) {
|
||||||
|
_scrollController.jumpTo(
|
||||||
|
index: firstElementInViewport.index + 1,
|
||||||
|
alignment: firstElementInViewport.itemLeadingEdge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstElementInViewport.index == 0) {
|
||||||
|
streamChannel.channel.markRead();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (widget.parentMessage != null) {
|
||||||
|
streamChannel.getReplies(widget.parentMessage.id);
|
||||||
|
}
|
||||||
|
|
||||||
_getOnThreadTap();
|
_getOnThreadTap();
|
||||||
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _getOnThreadTap() {
|
void _getOnThreadTap() {
|
||||||
@@ -696,7 +681,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_streamListener.cancel();
|
_messageNewListener?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -729,37 +730,68 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
return SizedBox();
|
return SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _wrapTextInBubble({
|
||||||
|
BuildContext context,
|
||||||
|
Widget child,
|
||||||
|
}) {
|
||||||
|
return Material(
|
||||||
|
shape: widget.shape ??
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
side: widget.borderSide ??
|
||||||
|
BorderSide(
|
||||||
|
color: Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Colors.white.withAlpha(24)
|
||||||
|
: Colors.black.withAlpha(24),
|
||||||
|
),
|
||||||
|
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
|
||||||
|
),
|
||||||
|
color: _getBackgroundColor(),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildTextBubble(BuildContext context) {
|
Widget _buildTextBubble(BuildContext context) {
|
||||||
|
final isOnlyEmoji =
|
||||||
|
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||||
|
|
||||||
|
Widget child = Transform(
|
||||||
|
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Padding(
|
||||||
|
padding: widget.textPadding,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
getFailedMessageWidget(context),
|
||||||
|
widget.textBuilder != null
|
||||||
|
? widget.textBuilder(context, widget.message)
|
||||||
|
: MessageText(
|
||||||
|
onLinkTap: widget.onLinkTap,
|
||||||
|
message: widget.message,
|
||||||
|
onMentionTap: widget.onMentionTap,
|
||||||
|
messageTheme: isOnlyEmoji
|
||||||
|
? widget.messageTheme.copyWith(
|
||||||
|
messageText:
|
||||||
|
widget.messageTheme.messageText.copyWith(
|
||||||
|
fontSize: 40,
|
||||||
|
))
|
||||||
|
: widget.messageTheme,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOnlyEmoji) {
|
||||||
|
child = _wrapTextInBubble(
|
||||||
|
context: context,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => retryMessage(context),
|
onTap: () => retryMessage(context),
|
||||||
onLongPress: () => onLongPress(context),
|
onLongPress: () => onLongPress(context),
|
||||||
child: Material(
|
child: child,
|
||||||
shape: widget.shape ??
|
|
||||||
RoundedRectangleBorder(
|
|
||||||
side: widget.borderSide ??
|
|
||||||
BorderSide(
|
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
|
||||||
? Colors.white.withAlpha(24)
|
|
||||||
: Colors.black.withAlpha(24),
|
|
||||||
),
|
|
||||||
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
|
|
||||||
),
|
|
||||||
color: _getBackgroundColor(),
|
|
||||||
child: Transform(
|
|
||||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Padding(
|
|
||||||
padding: widget.textPadding,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
getFailedMessageWidget(context),
|
|
||||||
_buildText(context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,15 +825,4 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildText(BuildContext context) {
|
|
||||||
return widget.textBuilder != null
|
|
||||||
? widget.textBuilder(context, widget.message)
|
|
||||||
: MessageText(
|
|
||||||
onLinkTap: widget.onLinkTap,
|
|
||||||
message: widget.message,
|
|
||||||
onMentionTap: widget.onMentionTap,
|
|
||||||
messageTheme: widget.messageTheme,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ dependencies:
|
|||||||
flutter_app_badger: ^1.1.2
|
flutter_app_badger: ^1.1.2
|
||||||
photo_view: ^0.10.1
|
photo_view: ^0.10.1
|
||||||
rxdart: ^0.24.1
|
rxdart: ^0.24.1
|
||||||
|
scrollable_positioned_list: ^0.1.8
|
||||||
jiffy: ^3.0.1
|
jiffy: ^3.0.1
|
||||||
|
flutter_svg: ^0.19.1
|
||||||
flutter_portal: ^0.3.0
|
flutter_portal: ^0.3.0
|
||||||
cached_network_image: ^2.2.0+1
|
cached_network_image: ^2.2.0+1
|
||||||
flutter_markdown: ^0.5.0
|
flutter_markdown: ^0.5.0
|
||||||
@@ -25,18 +27,20 @@ dependencies:
|
|||||||
image_picker: ^0.6.7+2
|
image_picker: ^0.6.7+2
|
||||||
flutter_keyboard_visibility: ^3.2.1
|
flutter_keyboard_visibility: ^3.2.1
|
||||||
stream_chat: ^0.2.10+1
|
stream_chat: ^0.2.10+1
|
||||||
|
emojis: ^0.9.3
|
||||||
mime: ^0.9.6+3
|
mime: ^0.9.6+3
|
||||||
visibility_detector: ^0.1.5
|
visibility_detector: ^0.1.5
|
||||||
http_parser: ^3.1.4
|
http_parser: ^3.1.4
|
||||||
|
substring_highlight: ^0.1.2
|
||||||
flutter_slidable: ^0.5.4
|
flutter_slidable: ^0.5.4
|
||||||
carousel_slider: ^2.2.1
|
carousel_slider: ^2.2.1
|
||||||
clipboard: ^0.1.2+8
|
clipboard: ^0.1.2+8
|
||||||
widgets_visibility_provider: ^2.0.2
|
|
||||||
photo_gallery: ^0.3.0
|
photo_gallery: ^0.3.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
assets:
|
assets:
|
||||||
- images/giphy_icon.png
|
- images/
|
||||||
|
- svgs/
|
||||||
fonts:
|
fonts:
|
||||||
- family: stream-icons
|
- family: stream-icons
|
||||||
fonts:
|
fonts:
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 24C18.6274 24 24 18.6274 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24Z" fill="black"/>
|
||||||
|
<rect x="7" y="7" width="1" height="10" fill="#00FF99"/>
|
||||||
|
<rect x="16" y="10" width="1" height="8" fill="#9D34FF"/>
|
||||||
|
<rect x="7" y="6" width="7" height="1" fill="#FFFF9C"/>
|
||||||
|
<rect x="7" y="17" width="9" height="1" fill="#00CCFF"/>
|
||||||
|
<path d="M14 6H15V7.5H16V9H17V10H14V6Z" fill="#FF6666"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 527 B |