imessage fix (#6)

* first draft : static views wip

* refactor to ChatMessage and ChatMessageHeader

* add pedantic

* fix lints

* remove hardcoded message

* appBar title : iMessage

* format date

* cupertino style and model view

* fix display of message based on isReceived or sent

* SendMessageInput

* refactor to MessagesPage

* MissingPluginException

* use builtins instead of custom stream builders

* handle isReceived

* delete channel header

* fix layout issues in ChannelPreview

* fix formatDate

* sticky message input

* usage of builtin MessageListCore

* remove unused imports and a todo

* rename channelsStates to channels

* replace file imports /w package:imessage/filename

* remove size required ChannelNameText

* lint and format

* sendMessage

* transition animation

* stream_chat_flutter_core instead

* format Date differently if same week

* add padding to bubble

* remove iMessage text from header

* reverse message order

* if same day don't display date

* padding chat bubble right

* remove unused extension

* constraint message width

* group messages By dates

* unused import

* fix bug input + layout+ preview onTap

* change updatedAt to createdAt

* add filter on channel

* handle refresh

* handle pagination with core LazyLoadScrollView

* preview simple medias

* upload bug

* fix attachment

* visual changes to message input

* remove size

* clean up

* wait 5 sec before displaying image(hack)

* fix a couple of things

* fix padding in channel preview

* fix upload + debugShowCheckedModeBanner false

* fix overflow and refactor to Divider

* fix android build

* add imessage clone hero image

* update README

Co-authored-by: Sacha Arbonel <[email protected]>
This commit is contained in:
Salvatore Giordano
2021-02-26 18:06:54 +01:00
committed by GitHub
co-authored by Sacha Arbonel
parent 6feccd57dd
commit f861eb4584
21 changed files with 344 additions and 279 deletions
+32 -29
View File
@@ -13,35 +13,38 @@ class ChannelListView extends StatelessWidget {
channels.removeWhere((channel) => channel.lastMessageAt == null);
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
//TODO: separators
return Column(
children: [
ChannelPreview(
channel: channels[index],
onTap: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => StreamChannel(
channel: channels[index],
child: MessagePage(),
),
transitionsBuilder: (
_,
animation,
secondaryAnimation,
child,
) =>
SharedAxisTransition(
child: child,
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
),
));
})
],
(
BuildContext context,
int index,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: ChannelPreview(
channel: channels[index],
onTap: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => StreamChannel(
channel: channels[index],
child: MessagePage(),
),
transitionsBuilder: (
_,
animation,
secondaryAnimation,
child,
) =>
SharedAxisTransition(
child: child,
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
),
),
);
},
),
);
},
childCount: channels.length,
+10 -6
View File
@@ -2,8 +2,11 @@ import 'package:flutter/cupertino.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Channel;
class ChannelNameText extends StatelessWidget {
const ChannelNameText({Key key, @required this.channel, this.size = 12})
: super(key: key);
const ChannelNameText({
Key key,
@required this.channel,
this.size = 17,
}) : super(key: key);
final Channel channel;
final double size;
@@ -11,11 +14,12 @@ class ChannelNameText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
channel.extraData['name'] as String ?? channel.config.name,
channel.extraData['name'] as String ?? 'No name',
style: TextStyle(
fontSize: size,
fontWeight: FontWeight.bold,
color: CupertinoColors.black),
fontSize: size,
fontWeight: FontWeight.bold,
color: CupertinoColors.black,
),
);
}
}
+69 -47
View File
@@ -9,6 +9,7 @@ import 'utils.dart';
class ChannelPreview extends StatelessWidget {
final VoidCallback onTap;
final Channel channel;
const ChannelPreview({
Key key,
@required this.onTap,
@@ -24,9 +25,9 @@ class ChannelPreview extends StatelessWidget {
? lastMessage?.attachments //TODO: ugly
?.map((e) {
if (e.type == 'image') {
return '📷';
return '📷 ';
} else if (e.type == 'video') {
return '🎬';
return '🎬 ';
}
return null;
})
@@ -35,57 +36,78 @@ class ChannelPreview extends StatelessWidget {
: '';
return GestureDetector(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: CupertinoColors.white),
behavior: HitTestBehavior.opaque,
child: Container(
constraints: BoxConstraints.tightFor(
height: 90,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ChannelImage(channel: channel, size: 50),
Padding(
padding:
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
child: ChannelImage(
channel: channel,
size: 50,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: ChannelNameText(
channel: channel,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ChannelNameText(
channel: channel,
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
isSameWeek(channel.lastMessageAt)
? formatDateSameWeek(channel.lastMessageAt)
: formatDate(channel.lastMessageAt),
style: TextStyle(
fontWeight: FontWeight.bold,
color: CupertinoColors.systemGrey),
),
)
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'$prefix ${lastMessage?.text ?? ''}',
style: TextStyle(
fontWeight: FontWeight.normal,
color: CupertinoColors.systemGrey),
overflow: TextOverflow.ellipsis,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
Text(
isSameWeek(channel.lastMessageAt)
? formatDateSameWeek(channel.lastMessageAt)
: formatDate(channel.lastMessageAt),
style: TextStyle(
fontSize: 15,
color: CupertinoColors.systemGrey,
),
),
Icon(
CupertinoIcons.right_chevron,
color: CupertinoColors.systemGrey3,
),
],
),
)
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'$prefix${lastMessage?.text ?? ''}',
style: TextStyle(
fontWeight: FontWeight.normal,
color: CupertinoColors.systemGrey,
fontSize: 16,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
Divider(),
],
),
)
],
+22 -21
View File
@@ -43,6 +43,7 @@ class IMessage extends StatelessWidget {
initializeDateFormatting('en_US', null);
return CupertinoApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: CupertinoThemeData(brightness: Brightness.light),
home: StreamChatCore(client: client, child: ChatLoader()),
);
@@ -50,31 +51,26 @@ class IMessage extends StatelessWidget {
}
class ChatLoader extends StatelessWidget {
const ChatLoader({
ChatLoader({
Key key,
}) : super(key: key);
final channelListController = ChannelListController();
@override
Widget build(BuildContext context) {
final user = StreamChatCore.of(context).user;
var channelListController = ChannelListController();
return CupertinoPageScaffold(
child: ChannelsBloc(
child: ChannelListCore(
channelListController: channelListController,
filter: {
r'$and': [
{
'members': {
r'$in': [user.id],
}
},
{
'type': {
r'$eq': 'messaging',
}
}
]
'members': {
r'$in': [user.id],
},
'type': {
r'$eq': 'messaging',
},
},
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
@@ -108,13 +104,18 @@ class ChatLoader extends StatelessWidget {
onEndOfPage: () async {
channelListController.paginateData();
},
child: CustomScrollView(slivers: [
CupertinoSliverRefreshControl(onRefresh: () async {
channelListController.loadData();
}),
ChannelPageAppBar(),
ChannelListView(channels: channels)
]),
child: CustomScrollView(
slivers: [
CupertinoSliverRefreshControl(onRefresh: () async {
channelListController.loadData();
}),
ChannelPageAppBar(),
SliverPadding(
sliver: ChannelListView(channels: channels),
padding: const EdgeInsets.only(top: 16),
)
],
),
))));
}
}
+4 -2
View File
@@ -9,8 +9,10 @@ class MessageHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final receivedAt = DateTime.parse(rawTimeStamp);
var textStyle = TextStyle(
fontWeight: FontWeight.bold, color: CupertinoColors.systemGrey);
final textStyle = TextStyle(
color: CupertinoColors.systemGrey,
fontSize: 14,
);
return isSameWeek(receivedAt)
? Text(
formatDateSameWeek(receivedAt),
+42 -38
View File
@@ -12,48 +12,52 @@ class MessageListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entries = groupBy(messages.reversed,
final entries = groupBy(messages,
(Message message) => message.createdAt.toString().substring(0, 10))
.entries
.toList();
return Stack(
return Column(
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.9,
child: Align(
alignment: FractionalOffset.topCenter,
child: ListView.builder(
shrinkWrap: true,
itemCount: entries.length,
itemBuilder: (context, index) {
return Column(
children: [
Padding(
padding:
const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 8.0),
child: MessageHeader(
rawTimeStamp: entries[index].key), //date
),
...entries[index].value //messages
.map((message) {
return MessageWidget(
alignment: isReceived(message, context)
? Alignment.centerLeft
: Alignment.topRight,
color: isReceived(message, context)
? CupertinoColors.systemGrey5
: CupertinoColors.systemBlue,
messageColor: isReceived(message, context)
? CupertinoColors.black
: CupertinoColors.white,
message: message,
);
}).toList()
],
);
}),
)),
Expanded(
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.9,
child: Align(
alignment: FractionalOffset.topCenter,
child: ListView.builder(
reverse: true,
itemCount: entries.length,
itemBuilder: (context, index) {
return Column(
children: [
Padding(
padding:
const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 8.0),
child: MessageHeader(
rawTimeStamp: entries[index].key), //date
),
...entries[index]
.value //messages
.map((message) {
return MessageWidget(
alignment: isReceived(message, context)
? Alignment.centerLeft
: Alignment.topRight,
color: isReceived(message, context)
? CupertinoColors.systemGrey5
: CupertinoColors.systemBlue,
messageColor: isReceived(message, context)
? CupertinoColors.black
: CupertinoColors.white,
message: message,
);
})
.toList()
.reversed,
],
);
}),
)),
),
MessageInput()
],
);
+8 -2
View File
@@ -21,8 +21,14 @@ class MessagePage extends StatelessWidget {
navigationBar: CupertinoNavigationBar(
middle: Column(
children: [
ChannelImage(size: 25, channel: streamChannel.channel),
ChannelNameText(channel: streamChannel.channel)
ChannelImage(
size: 25,
channel: streamChannel.channel,
),
ChannelNameText(
size: 16,
channel: streamChannel.channel,
),
],
),
), //ChannelHeader
+14 -17
View File
@@ -1,7 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:imessage/cutom_painter.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Message, AttachmentUploadStateBuilder;
import 'package:stream_chat_flutter/stream_chat_flutter.dart'
show Message, AttachmentUploadStateBuilder;
class MessageWidget extends StatelessWidget {
final Alignment alignment;
@@ -19,7 +20,7 @@ class MessageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (message.attachments.isNotEmpty &&
if (message.attachments?.isNotEmpty == true &&
message.attachments.first.type == "image") {
return MessageImage(
color: color, message: message, messageColor: messageColor);
@@ -58,21 +59,17 @@ class MessageImage extends StatelessWidget {
color: color,
child: Column(
children: [
FutureBuilder<String>( //hack: //AttachmentUploadStateBuilder once https://github.com/GetStream/stream-chat-flutter/pull/276 is merged
future: Future.delayed(
const Duration(seconds: 5),
() =>
message.attachments.first.thumbUrl ??
message.attachments.first.imageUrl ??
message.attachments.first.assetUrl),
builder: (context, snapshot) {
return snapshot.hasData
? CachedNetworkImage(
imageUrl: snapshot.data,
)
: CupertinoActivityIndicator();
}),
if (message.attachments.first.file != null)
Image.memory(
message.attachments.first.file.bytes,
fit: BoxFit.cover,
)
else
CachedNetworkImage(
imageUrl: message.attachments.first.thumbUrl ??
message.attachments.first.imageUrl ??
message.attachments.first.assetUrl,
),
if (message.attachments.first?.title != null)
Padding(
padding: const EdgeInsets.all(8.0),
+29 -4
View File
@@ -8,13 +8,17 @@ String formatDate(DateTime date) {
}
String formatDateSameWeek(DateTime date) {
final dateFormat = DateFormat('EEEE, hh:mm a');
DateFormat dateFormat;
if (date.day == DateTime.now().day) {
dateFormat = DateFormat('hh:mm a');
} else {
dateFormat = DateFormat('EEEE, hh:mm a');
}
return dateFormat.format(date);
}
String formatDateMessage(DateTime date) {
final dateFormat =
DateFormat('EEE. MMM. d ' 'yy' ' hh:mm a');
final dateFormat = DateFormat('EEE. MMM. d ' 'yy' ' hh:mm a');
return dateFormat.format(date);
}
@@ -35,7 +39,8 @@ class CupertinoCircleAvatar extends StatelessWidget {
height: size,
width: size,
fit: BoxFit.cover,
errorWidget: (context, url, error) { //TODO: this crash the app when getting 404 and in debug mode, see :https://github.com/Baseflow/flutter_cached_network_image/issues/504
errorWidget: (context, url, error) {
//TODO: this crash the app when getting 404 and in debug mode, see :https://github.com/Baseflow/flutter_cached_network_image/issues/504
return CachedNetworkImage(
imageUrl:
"https://4.bp.blogspot.com/-Jx21kNqFSTU/UXemtqPhZCI/AAAAAAAAh74/BMGSzpU6F48/s1600/funny-cat-pictures-047-001.jpg");
@@ -43,3 +48,23 @@ class CupertinoCircleAvatar extends StatelessWidget {
);
}
}
class Divider extends StatelessWidget {
const Divider({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Align(
child: Container(
height: 1,
color: CupertinoColors.systemGrey5,
),
alignment: Alignment.bottomCenter,
),
);
}
}