Commit 3da97bb6 authored by Wilko Manger's avatar Wilko Manger

Use composition, not inheritance for subtitles

parent a0390991
......@@ -16,6 +16,7 @@
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
import 'package:pattle/src/section/main/widgets/redacted.dart';
import '../../../../../../../util/color.dart';
......@@ -26,6 +27,10 @@ import '../../message.dart';
///
/// Must have a [MessageBubble] as ancestor.
class RedactedContent extends StatelessWidget {
final ChatMessage message;
const RedactedContent({Key key, this.message}) : super(key: key);
@override
Widget build(BuildContext context) {
final bubble = MessageBubble.of(context);
......
......@@ -19,6 +19,7 @@ import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
import 'package:pattle/src/section/main/widgets/message_state.dart';
import 'package:pattle/src/util/color.dart';
import 'package:pattle/src/util/date_format.dart';
import 'package:provider/provider.dart';
......@@ -327,12 +328,10 @@ class MessageInfo extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (bubble.message.isMine) ...[
if (MessageState.necessary(bubble.message)) ...[
Center(
child: Icon(
bubble.message.event.sentState != SentState.sent
? Icons.access_time
: Icons.check,
child: MessageState(
message: bubble.message,
color: Colors.white,
size: 14,
),
......
......@@ -17,6 +17,7 @@
import 'package:bloc/bloc.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
import '../../../matrix.dart';
import '../../../util/room.dart';
......@@ -29,12 +30,12 @@ import 'state.dart';
export 'state.dart';
class ChatsBloc extends Bloc<ChatsEvent, ChatsState> {
final Matrix matrix;
final Matrix _matrix;
ChatsBloc(this.matrix);
ChatsBloc(this._matrix);
Future<List<ChatOverview>> _getChats() async {
final me = matrix.user;
final me = _matrix.user;
await me.sync.first;
......@@ -78,8 +79,20 @@ class ChatsBloc extends Bloc<ChatsEvent, ChatsState> {
final chat = ChatOverview(
room: room,
name: room.name,
latestEvent: latestEvent,
latestEventForSorting: latestEventForSorting,
latestMessage: latestEvent != null
? ChatMessage(
room,
latestEvent,
isMine: latestEvent.sender == _matrix.user,
)
: null,
latestMessageForSorting: latestEventForSorting != null
? ChatMessage(
room,
latestEventForSorting,
isMine: latestEventForSorting.sender == _matrix.user,
)
: null,
);
chats.add(chat);
......@@ -98,16 +111,16 @@ class ChatsBloc extends Bloc<ChatsEvent, ChatsState> {
} else if (a.room.totalUnreadNotificationCount <= 0 &&
b.room.totalUnreadNotificationCount > 0) {
return -1;
} else if (a.latestEventForSorting != null &&
b.latestEventForSorting != null) {
return a.latestEventForSorting.time.compareTo(
b.latestEventForSorting.time,
} else if (a.latestMessageForSorting != null &&
b.latestMessageForSorting != null) {
return a.latestMessageForSorting.event.time.compareTo(
b.latestMessageForSorting.event.time,
);
} else if (a.latestEventForSorting != null &&
b.latestEventForSorting == null) {
} else if (a.latestMessageForSorting != null &&
b.latestMessageForSorting == null) {
return 1;
} else if (a.latestEventForSorting == null &&
b.latestEventForSorting != null) {
} else if (a.latestMessageForSorting == null &&
b.latestMessageForSorting != null) {
return -1;
} else {
return 0;
......
......@@ -17,6 +17,7 @@
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:meta/meta.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
/// Chat overview used in the 'chats' page.
class ChatOverview {
......@@ -24,13 +25,13 @@ class ChatOverview {
final String name;
final RoomEvent latestEvent;
final RoomEvent latestEventForSorting;
final ChatMessage latestMessage;
final ChatMessage latestMessageForSorting;
ChatOverview({
@required this.room,
@required this.name,
@required this.latestEvent,
@required this.latestEventForSorting,
@required this.latestMessage,
@required this.latestMessageForSorting,
});
}
......@@ -52,7 +52,7 @@ class ChatOverviewListState extends State<ChatOverviewList> {
}
Widget _buildChatOverview(ChatOverview chat) {
final time = formatAsListItem(context, chat.latestEvent?.time);
final time = formatAsListItem(context, chat.latestMessage?.event?.time);
return ListTile(
title: Row(
......@@ -79,7 +79,7 @@ class ChatOverviewListState extends State<ChatOverviewList> {
},
leading: ChatAvatar(room: chat.room),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
subtitle: Subtitle.forChat(chat),
subtitle: Subtitle.withContent(chat),
);
}
}
......@@ -16,35 +16,25 @@
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/section/main/widgets/message_state.dart';
import '../../../../matrix.dart';
import 'subtitle.dart';
class ImageSubtitle extends Subtitle {
class ImageSubtitleContent extends Subtitle {
@override
final ImageMessageEvent event;
Widget build(BuildContext context) {
final message = Subtitle.of(context).chat.latestMessage;
ImageSubtitle(Matrix matrix, Room room, this.event)
: super(matrix, room, event);
@override
Widget build(BuildContext context) => Row(
children: <Widget>[
buildSentStateIcon(context),
RichText(
text: senderSpan(context),
),
Icon(
Icons.photo_camera,
color: Theme.of(context).textTheme.caption.color,
size: Subtitle.iconSize,
),
Expanded(
child: Text(' ' + l(context).photo, style: textStyle(context)),
),
buildNotificationCount(context)
],
);
return Row(
children: <Widget>[
if (MessageState.necessary(message)) MessageState(message: message),
if (Sender.necessary(context)) Sender(),
Icon(Icons.photo_camera),
Expanded(
child: Text(' ' + l(context).photo),
),
],
);
}
}
// Copyright (C) 2019 Wilko Manger
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/chat/util/member_span.dart';
import '../../../../matrix.dart';
import 'subtitle.dart';
class MemberSubtitle extends Subtitle {
@override
final MemberChangeEvent event;
MemberSubtitle(Matrix matrix, Room room, this.event)
: super(matrix, room, event);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: textStyle(context),
children: spanFor(context, event),
),
),
),
buildNotificationCount(context)
],
);
}
}
// Copyright (C) 2019 Wilko Manger
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/widgets/redacted.dart';
import '../../../../matrix.dart';
import 'subtitle.dart';
class RedactedSubtitle extends Subtitle {
@override
final RedactedEvent event;
RedactedSubtitle(Matrix matrix, Room room, this.event)
: super(matrix, room, event);
@override
Widget build(BuildContext context) => Row(
children: <Widget>[
Expanded(
child: Redacted(
event: event,
iconSize: 20,
color: Theme.of(context).textTheme.caption.color,
),
),
buildNotificationCount(context)
],
);
}
......@@ -18,99 +18,130 @@
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/chat/widgets/bubble/state/content/creation.dart';
import 'package:pattle/src/section/main/chat/widgets/bubble/state/content/member_change.dart';
import 'package:pattle/src/section/main/chat/widgets/bubble/state/content/topic_change.dart';
import 'package:pattle/src/section/main/chat/widgets/bubble/state/content/upgrade.dart';
import 'package:pattle/src/section/main/chats/models/chat_overview.dart';
import 'package:provider/provider.dart';
import '../../../../matrix.dart';
import '../../../../util/user.dart';
import 'image_subtitle.dart';
import 'member_subtitle.dart';
import 'redacted_subtitle.dart';
import 'text_subtitle.dart';
import 'topic_subtitle.dart';
import 'typing_subtitle.dart';
import 'unsupported_subtitle.dart';
abstract class Subtitle extends StatelessWidget {
static const iconSize = 20.0;
final Room room;
final RoomEvent event;
@protected
final String senderName;
final bool isMine;
Subtitle(Matrix matrix, this.room, this.event)
: isMine = event?.sender == matrix.user,
senderName =
event != null && event.sender != matrix.user && !room.isDirect
? '${event.sender.displayName}: '
: '';
static Widget forChat(ChatOverview chat) {
return Builder(
builder: (context) {
final matrix = Matrix.of(context);
// TODO: typingUsers should not contain nulls
if (chat.room.isSomeoneElseTyping &&
!chat.room.typingUsers.any((u) => u == null)) {
return TypingSubtitle(matrix, chat.room);
} else {
final event = chat.latestEvent;
if (event == null) {
return UnsupportedSubtitle(matrix, chat.room, event);
}
if (event is TextMessageEvent) {
return TextSubtitle(matrix, chat.room, event);
} else if (event is ImageMessageEvent) {
return ImageSubtitle(matrix, chat.room, event);
} else if (event is MemberChangeEvent) {
return MemberSubtitle(matrix, chat.room, event);
} else if (event is RedactedEvent) {
return RedactedSubtitle(matrix, chat.room, event);
} else if (event is TopicChangeEvent) {
return TopicSubtitle(matrix, chat.room, event);
}
return UnsupportedSubtitle(matrix, chat.room, event);
}
},
);
}
class Subtitle extends StatelessWidget {
final ChatOverview chat;
final Widget child;
TextStyle textStyle(BuildContext context) => Theme.of(context)
.textTheme
.body1
.copyWith(color: Theme.of(context).textTheme.caption.color);
const Subtitle({Key key, this.chat, this.child}) : super(key: key);
TextSpan senderSpan(BuildContext context, {String sender}) => TextSpan(
text: sender ?? senderName,
style: Theme.of(context).textTheme.body1.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.caption.color,
),
);
Widget buildSentStateIcon(BuildContext context) {
if (isMine) {
return Icon(
event.sentState != SentState.sent ? Icons.access_time : Icons.check,
size: Subtitle.iconSize,
color: Colors.grey,
);
static Widget withContent(ChatOverview chat) {
Widget content;
// TODO: typingUsers should not contain nulls
if (chat.room.isSomeoneElseTyping &&
!chat.room.typingUsers.any((u) => u == null)) {
content = TypingSubtitleContent();
} else {
return Container(height: 0, width: 0);
final event = chat.latestMessage?.event;
if (event == null) {
content = UnsupportedSubtitleContent();
} else if (event is TextMessageEvent) {
content = TextSubtitleContent();
} else if (event is ImageMessageEvent) {
content = ImageSubtitleContent();
} else if (event is MemberChangeEvent) {
content = MemberChangeContent(message: chat.latestMessage);
} else if (event is RedactedEvent) {
content = RedactedSubtitleContent();
} else if (event is TopicChangeEvent) {
content = TopicChangeContent(message: chat.latestMessage);
} else if (event is RoomUpgradeEvent) {
content = UpgradeContent(message: chat.latestMessage);
} else if (event is RoomCreationEvent) {
content = CreationContent(message: chat.latestMessage);
} else {
content = UnsupportedSubtitleContent();
}
}
return Subtitle(
chat: chat,
child: content,
);
}
Widget buildNotificationCount(BuildContext context) {
if (room.totalUnreadNotificationCount <= 0) {
return Container();
}
static Subtitle of(BuildContext context) =>
Provider.of<Subtitle>(context, listen: false);
@override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: Theme.of(context).textTheme.body1.copyWith(
color: Theme.of(context).textTheme.caption.color,
),
child: Provider<Subtitle>.value(
value: this,
// Builder is necessary to get context with
// correct DefaultTextStyle.
child: Builder(
builder: (context) {
return Row(
children: <Widget>[
Expanded(
child: IconTheme(
data: IconThemeData(
color: DefaultTextStyle.of(context).style.color,
size: 20,
),
child: child,
),
),
if (_NotificationCount.necessary(context)) _NotificationCount(),
],
);
},
),
),
);
}
}
class Sender extends StatelessWidget {
const Sender({Key key}) : super(key: key);
static bool necessary(BuildContext context) {
final message = Subtitle.of(context).chat.latestMessage;
return message.event.sender != Matrix.of(context).user &&
!message.room.isDirect;
}
@override
Widget build(BuildContext context) {
final message = Subtitle.of(context).chat.latestMessage;
return Text(
'${message.event.sender.displayName}: ',
maxLines: 1,
style: TextStyle(fontWeight: FontWeight.bold),
);
}
}
class _NotificationCount extends StatelessWidget {
static bool necessary(BuildContext context) {
return Subtitle.of(context).chat.room.totalUnreadNotificationCount > 0;
}
@override
Widget build(BuildContext context) {
final room = Subtitle.of(context).chat.room;
return SizedBox(
height: 21,
......@@ -125,7 +156,7 @@ abstract class Subtitle extends StatelessWidget {
child: Center(
child: Text(
room.totalUnreadNotificationCount.toString(),
style: textStyle(context).copyWith(
style: TextStyle(
fontSize: 13,
color: Colors.white,
fontWeight: FontWeight.w500,
......
......@@ -17,81 +17,48 @@
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import '../../../../matrix.dart';
import '../../../../util/user.dart';
import 'package:pattle/src/section/main/widgets/message_state.dart';
import 'subtitle.dart';
class TextSubtitle extends Subtitle {
@override
final TextMessageEvent event;
TextSubtitle(Matrix matrix, Room room, this.event)
: super(matrix, room, event);
class TextSubtitleContent extends StatelessWidget {
TextSubtitleContent({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final sender = senderSpan(
context,
sender: event is EmoteMessageEvent
? event.sender.getDisplayName(context) + ' '
: null,
);
if (event.content.inReplyToId == null) {
return Row(
children: <Widget>[
buildSentStateIcon(context),
Expanded(
child: RichText(
overflow: TextOverflow.ellipsis,
maxLines: 1,
text: TextSpan(
style: textStyle(context),
children: [
sender,
TextSpan(text: event.content.body ?? 'null')
],
),
),
),
buildNotificationCount(context)
],
);
} else {
final message = Subtitle.of(context).chat.latestMessage;
final event = message.event as TextMessageEvent;
final isReply = event.content.inReplyToId != null;
var text = event.content.formattedBody ?? event.content.body;
if (isReply) {
// Strip replied-to content
var text = event.content.formattedBody;
final splitReply = text.split(RegExp('(<\\/*mx-reply>)'));
if (splitReply.length >= 3) {
text = splitReply[2];
text = ' ' + splitReply[2];
}
}
return Row(
children: <Widget>[
buildSentStateIcon(context),
RichText(
return Row(
children: <Widget>[
if (MessageState.necessary(message)) ...[
MessageState(
message: message,
),
SizedBox(width: 4),
],
if (Sender.necessary(context)) Sender(),
if (isReply) Icon(Icons.reply),
if (event is EmoteMessageEvent) Sender(),
Expanded(
child: Text(
text ?? 'null',
overflow: TextOverflow.ellipsis,
maxLines: 1,
text: sender,
),
Icon(
Icons.reply,
color: Theme.of(context).textTheme.caption.color,
size: Subtitle.iconSize,
),
Expanded(
child: RichText(
overflow: TextOverflow.ellipsis,
maxLines: 1,
text: TextSpan(
style: textStyle(context),
text: ' ' + text ?? 'null',
),
),
),
buildNotificationCount(context)
],
);
}
),
],
);
}
}
// Copyright (C) 2019 Wilko Manger
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/localizations.dart';
import '../../../../matrix.dart';
import '../../../../util/user.dart';
import 'subtitle.dart';
class TopicSubtitle extends Subtitle {
@override
final TopicChangeEvent event;
TopicSubtitle(Matrix matrix, Room room, this.event)
: super(matrix, room, event);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: textStyle(context),
children: l(context).changedDescription(
TextSpan(
text: event.sender.displayName,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
),
),
buildNotificationCount(context)
],
);
}
}
......@@ -16,38 +16,26 @@
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/chat/util/typing_span.dart';
import '../../../../matrix.dart';
import 'subtitle.dart';
class TypingSubtitle extends Subtitle {