...
 
Commits (7)
{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"device_info","dependencies":[]},{"name":"firebase_core","dependencies":[]},{"name":"firebase_messaging","dependencies":[]},{"name":"flutter_local_notifications","dependencies":[]},{"name":"image_picker","dependencies":[]},{"name":"moor_ffi","dependencies":[]},{"name":"package_info","dependencies":[]},{"name":"path_provider","dependencies":[]},{"name":"respect_24_hour","dependencies":[]},{"name":"shared_preferences","dependencies":[]},{"name":"sqflite","dependencies":[]},{"name":"url_launcher","dependencies":[]}]}
\ No newline at end of file
......@@ -41,6 +41,7 @@ version
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
......
......@@ -24,12 +24,12 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:meta/meta.dart';
import 'package:bloc/bloc.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/chats/models/chat.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
import '../auth/bloc.dart';
import '../matrix.dart';
import '../util/room.dart';
import '../util/user.dart';
import '../util/url.dart';
import 'event.dart';
......@@ -115,7 +115,7 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
static Message _eventToMessage(RoomEvent event, Person person) {
if (event is EmoteMessageEvent) {
return Message(
'${event.sender.displayName} ${event.content.body}',
'${person.name} ${event.content.body}',
event.time,
person,
);
......@@ -150,15 +150,19 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
final room = await user.rooms[message.roomId];
final event = await room.timeline[message.eventId];
final senderName = event.sender.displayName;
final icon = await DefaultCacheManager().getSingleFile(
event.sender.avatarUrl.toThumbnailStringWith(user.homeserver),
);
final sender = await ChatMember.fromUser(
room,
event.sender,
isYou: false,
);
final senderPerson = Person(
bot: false,
name: senderName,
name: sender.name,
icon: icon.path,
iconSource: IconSource.FilePath,
);
......@@ -170,9 +174,11 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
return;
}
final chat = Chat(room: room);
await plugin.show(
event.id.hashCode,
room.getDisplayName(),
chat.name,
message.text,
NotificationDetails(
AndroidNotificationDetails(
......@@ -186,8 +192,7 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
style: AndroidNotificationStyle.Messaging,
styleInformation: MessagingStyleInformation(
senderPerson,
conversationTitle:
!room.isDirect ? await room.getDisplayName() : null,
conversationTitle: !room.isDirect ? await chat.name : null,
groupConversation: room.isDirect,
messages: [message],
),
......
......@@ -172,20 +172,20 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
final inReplyToEvent = await room.timeline[event.content.inReplyToId];
if (inReplyToEvent != null) {
inReplyTo = ChatMessage(
inReplyTo = await ChatMessage.create(
room,
inReplyToEvent,
isMine: inReplyToEvent.sender == me,
isMe: (u) => u == matrix.user,
);
}
}
messages.add(
ChatMessage(
await ChatMessage.create(
room,
event,
inReplyTo: inReplyTo,
isMine: event.sender == me,
isMe: (u) => u == matrix.user,
),
);
}
......
......@@ -18,6 +18,7 @@ import 'dart:async';
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 'event.dart';
......@@ -54,7 +55,13 @@ class ImageBloc extends Bloc<ImageEvent, ImageState> {
}
}
yield ImagesLoaded(imageMessageEvents);
final messages = await Future.wait(
imageMessageEvents.map(
(i) => ChatMessage.create(_room, i, isMe: (u) => u == _matrix.user),
),
);
yield ImagesLoaded(messages);
}
}
......
......@@ -19,28 +19,28 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import '../../../../matrix.dart';
import '../../../../util/date_format.dart';
import '../../../../util/user.dart';
import '../../../../util/url.dart';
import 'bloc.dart';
class ImagePage extends StatefulWidget {
final ImageMessageEvent event;
final ChatMessage message;
ImagePage._(this.event);
ImagePage._(this.message);
static Widget withBloc(ChatMessage message) {
assert(message.event is ImageMessageEvent);
return BlocProvider<ImageBloc>(
create: (c) => ImageBloc(Matrix.of(c), message.room),
child: ImagePage._(message.event),
child: ImagePage._(message),
);
}
......@@ -49,7 +49,7 @@ class ImagePage extends StatefulWidget {
}
class _ImagePageState extends State<ImagePage> {
User _messageSender;
ChatMember _messageSender;
String _date;
@override
......@@ -63,10 +63,10 @@ class _ImagePageState extends State<ImagePage> {
BlocProvider.of<ImageBloc>(context).add(FetchImages());
_messageSender = widget.event.sender;
_messageSender = widget.message.sender;
_date = '${formatAsDate(context, widget.event.time)},'
' ${formatAsTime(widget.event.time)}';
_date = '${formatAsDate(context, widget.message.event.time)},'
' ${formatAsTime(widget.message.event.time)}';
}
@override
......@@ -85,13 +85,13 @@ class _ImagePageState extends State<ImagePage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_messageSender.displayName),
Text(_messageSender.name),
SizedBox(height: 2),
Text(
_date,
style: Theme.of(context)
.textTheme
.body1
.bodyText2
.copyWith(color: Colors.white),
),
],
......@@ -108,28 +108,30 @@ class _ImagePageState extends State<ImagePage> {
return BlocBuilder<ImageBloc, ImageState>(
builder: (context, state) {
if (state is ImagesLoaded) {
final events = state.events;
final messages = state.messages;
return PhotoViewGallery.builder(
itemCount: events.length,
itemCount: messages.length,
reverse: true,
builder: (context, index) {
final event = messages[index].event as ImageMessageEvent;
return PhotoViewGalleryPageOptions(
imageProvider: CachedNetworkImageProvider(
events[index].content.url.toDownloadString(context),
event.content.url.toDownloadString(context),
),
heroTag: widget.event.id,
heroTag: widget.message.event.id,
minScale: PhotoViewComputedScale.contained,
);
},
onPageChanged: (index) {
setState(() {
_messageSender = events[index].sender;
_date = '${formatAsDate(context, events[index].time)}, '
'${formatAsTime(events[index].time)}';
_messageSender = messages[index].sender;
_date = '${formatAsDate(context, messages[index].event.time)}, '
'${formatAsTime(messages[index].event.time)}';
});
},
pageController: PageController(
initialPage: events.indexOf(widget.event),
initialPage: messages.indexOf(widget.message),
),
);
}
......
import 'package:equatable/equatable.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
abstract class ImageState extends Equatable {
@override
......@@ -11,10 +11,10 @@ class ImagesUninitialized extends ImageState {}
class ImagesLoading extends ImageState {}
class ImagesLoaded extends ImageState {
final List<ImageMessageEvent> events;
final List<ChatMessage> messages;
ImagesLoaded(this.events);
ImagesLoaded(this.messages);
@override
List<Object> get props => [events];
List<Object> get props => [messages];
}
......@@ -27,17 +27,16 @@ import 'package:pattle/src/app.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/chats/models/chat.dart';
import 'package:pattle/src/section/main/chats/widgets/typing_content.dart';
import 'package:pattle/src/section/main/widgets/chat_name.dart';
import 'package:pattle/src/section/main/widgets/error.dart';
import 'package:pattle/src/section/main/widgets/title_with_sub.dart';
import '../../../matrix.dart';
import '../../../util/color.dart';
import '../../../util/room.dart';
import '../../../util/url.dart';
import 'bloc.dart';
import 'util/typing_span.dart';
import 'widgets/bubble/message.dart';
import 'widgets/bubble/state.dart';
import 'widgets/date_header.dart';
......@@ -88,7 +87,7 @@ class _ChatPageState extends State<ChatPage> {
@override
Widget build(BuildContext context) {
Widget avatar = Container();
final avatarUrl = widget.chat.room.displayAvatarUrl;
final avatarUrl = widget.chat.avatarUrl;
if (avatarUrl != null) {
avatar = Hero(
tag: _room.id,
......@@ -114,11 +113,7 @@ class _ChatPageState extends State<ChatPage> {
_room.isSomeoneElseTyping && !_room.typingUsers.any((u) => u == null)
? TitleWithSub(
title: ChatName(chat: widget.chat),
subtitle: RichText(
text: TextSpan(
children: typingSpan(context, _room),
),
),
subtitle: TypingContent(chat: widget.chat),
)
: ChatName(chat: widget.chat);
......
......@@ -19,6 +19,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
import '../../../../matrix.dart';
import 'event.dart';
......@@ -41,14 +42,20 @@ class ChatSettingsBloc extends Bloc<ChatSettingsEvent, ChatSettingsState> {
if (event is FetchMembers) {
final me = await _room.members[_matrix.user.id];
var members = List.of(
var user = List.of(
await _room.members.get(upTo: !event.all ? 6 : _room.members.count),
);
members = members.where((u) => u.state.membership is Joined).toList();
user = user.where((u) => u.state.membership is Joined).toList();
members.remove(me);
members.insert(0, me);
user.remove(me);
user.insert(0, me);
final members = await Future.wait(
user.map(
(u) => ChatMember.fromUser(_room, u, isYou: _matrix.user == u),
),
);
yield MembersLoaded(members);
}
......
......@@ -24,13 +24,13 @@ import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/chats/models/chat.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
import 'package:pattle/src/section/main/widgets/chat_name.dart';
import 'package:pattle/src/section/main/widgets/user_item.dart';
import 'package:pattle/src/section/main/widgets/chat_member_tile.dart';
import '../../../../matrix.dart';
import '../../../../util/url.dart';
import '../../../../util/color.dart';
import '../../../../util/room.dart';
import 'bloc.dart';
class ChatSettingsPage extends StatefulWidget {
......@@ -68,8 +68,7 @@ class _ChatSettingsPageState extends State<ChatSettingsPage> {
),
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
final url =
widget.chat.room.displayAvatarUrl?.toDownloadString(context);
final url = widget.chat.avatarUrl?.toDownloadString(context);
return <Widget>[
SliverAppBar(
expandedHeight: 128.0,
......@@ -200,7 +199,7 @@ class _MemberListState extends State<_MemberList> {
SizedBox(height: 4),
BlocBuilder<ChatSettingsBloc, ChatSettingsState>(
builder: (context, state) {
var members = List<User>();
var members = List<ChatMember>();
if (state is MembersLoaded) {
members = state.members;
}
......@@ -235,8 +234,8 @@ class _MemberListState extends State<_MemberList> {
);
}
return UserItem(
user: members[index],
return ChatMemberTile(
member: members[index],
);
},
),
......
import 'package:equatable/equatable.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
abstract class ChatSettingsState extends Equatable {
@override
......@@ -11,7 +11,7 @@ class ChatSettingsUninitialized extends ChatSettingsState {}
class MembersLoading extends ChatSettingsState {}
class MembersLoaded extends ChatSettingsState {
final List<User> members;
final List<ChatMember> members;
MembersLoaded(this.members);
......
......@@ -54,7 +54,7 @@ class RedactedContent extends StatelessWidget {
dark: bubble.message.isMine ? Colors.white30 : Colors.white70,
),
),
child: Redacted(event: bubble.message.event),
child: Redacted(redaction: bubble.message.redaction),
),
if (MessageInfo.necessary(context))
Padding(
......
......@@ -21,7 +21,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_html/flutter_html.dart';
import '../../../../../../../util/color.dart';
import '../../../../../../../util/user.dart';
import '../../../../../../../util/chat_member.dart';
import '../../message.dart';
......@@ -45,7 +45,7 @@ class TextContent extends StatelessWidget {
? _ReplyBorderPainter(
color: bubble.message.isMine && bubble.reply?.isMine == true
? Colors.white
: bubble.message.event.sender.getColor(context),
: bubble.message.sender.color(context),
borderRadius: bubble.borderRadius,
)
: null,
......
......@@ -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/chat/widgets/bubble/message/content/loading.dart';
import 'package:pattle/src/section/main/models/chat_member.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';
......@@ -29,7 +30,7 @@ import 'content/image.dart';
import 'content/redacted.dart';
import 'content/text.dart';
import '../../../../../../util/user.dart';
import '../../../../../../util/chat_member.dart';
class MessageBubble extends StatelessWidget {
final ChatMessage message;
......@@ -144,7 +145,18 @@ class MessageBubble extends StatelessWidget {
body: 'Blabla',
),
),
isMine: isMine,
sender: ChatMember(
User(
id: UserId('@wilko:pattle.im'),
state: UserState(
roomId: RoomId('!343432:pattle.im'),
displayName: 'Wilko',
since: DateTime.now(),
),
),
isYou: isMine,
name: 'Wilko',
),
),
child: LoadingContent(),
);
......@@ -167,7 +179,7 @@ class MessageBubble extends StatelessWidget {
}
final previousHasSameSender = previousEvent != null &&
previousEvent.sender.displayName == event.sender.displayName &&
previousMessage.sender.name == previousMessage.sender.name &&
previousEvent.sender == event.sender;
if (!previousHasSameSender) {
......@@ -201,7 +213,7 @@ class MessageBubble extends StatelessWidget {
}
final nextHasSameSender = nextEvent != null &&
nextEvent.sender.displayName == event.sender.displayName &&
nextMessage.sender.name == nextMessage.sender.name &&
nextEvent.sender == event.sender;
if (!nextHasSameSender) {
......@@ -290,7 +302,7 @@ class MessageBubble extends StatelessWidget {
elevation: 1,
shape: border,
child: DefaultTextStyle(
style: Theme.of(context).textTheme.body1.apply(
style: Theme.of(context).textTheme.bodyText2.apply(
fontSizeFactor: 1.1,
color: message.isMine ? Colors.white : null,
),
......@@ -419,12 +431,10 @@ class Sender extends StatelessWidget {
final bubble = MessageBubble.of(context);
return Text(
bubble.message.event.sender.displayName,
bubble.message.sender.name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: personalizedColor
? bubble.message.event.sender.getColor(context)
: null,
color: personalizedColor ? bubble.message.sender.color(context) : null,
),
);
}
......
......@@ -20,8 +20,6 @@ import 'package:flutter/material.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
import '../../../../../../../util/user.dart';
import '../state.dart';
/// If [message] is `null`, will try to get the [message] from the
......@@ -40,7 +38,10 @@ class CreationContent extends StatelessWidget {
style: DefaultTextStyle.of(context).style,
children: l(context).createdThisGroup(
TextSpan(
text: message.room.creator.displayName,
text: message.sender.name,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
......
......@@ -17,10 +17,10 @@
// 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/models/chat_message.dart';
import '../../../../util/member_span.dart';
import '../state.dart';
/// If [message] is `null`, will try to get the [message] from the
......@@ -30,6 +30,34 @@ class MemberChangeContent extends StatelessWidget {
const MemberChangeContent({Key key, this.message}) : super(key: key);
List<TextSpan> _span(BuildContext context, ChatMessage message) {
final event = message.event;
final style = TextStyle(fontWeight: FontWeight.bold);
final sender = TextSpan(
text: message.sender.name,
style: style,
);
final subject = TextSpan(
text: message.subject.name,
style: style,
);
var text;
if (event is JoinEvent) {
text = l(context).joined(subject);
} else if (event is LeaveEvent) {
text = l(context).left(subject);
} else if (event is InviteEvent) {
text = l(context).wasInvitedBy(subject, sender);
} else if (event is BanEvent) {
text = l(context).wasBannedBy(subject, sender);
}
return text;
}
@override
Widget build(BuildContext context) {
final message = this.message ?? StateBubble.of(context).message;
......@@ -37,10 +65,7 @@ class MemberChangeContent extends StatelessWidget {
return RichText(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: spanFor(
context,
message.event,
),
children: _span(context, message),
),
);
}
......
......@@ -20,8 +20,6 @@ import 'package:flutter/material.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
import '../../../../../../../util/user.dart';
import '../state.dart';
/// If [message] is `null`, will try to get the [message] from the
......@@ -40,7 +38,7 @@ class TopicChangeContent extends StatelessWidget {
style: DefaultTextStyle.of(context).style,
children: l(context).changedDescriptionTapToView(
TextSpan(
text: message.event.sender.displayName,
text: message.sender.name,
),
),
),
......
......@@ -20,8 +20,6 @@ import 'package:flutter/material.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
import '../../../../../../../util/user.dart';
import '../state.dart';
/// If [message] is `null`, will try to get the [message] from the
......@@ -40,7 +38,7 @@ class UpgradeContent extends StatelessWidget {
style: DefaultTextStyle.of(context).style,
children: l(context).upgradedThisGroup(
TextSpan(
text: message.event.sender.displayName,
text: message.sender.name,
),
),
),
......
......@@ -33,7 +33,7 @@ class DateHeader extends StatelessWidget {
Center(
child: Text(
formatAsDate(context, date).toUpperCase(),
style: Theme.of(context).textTheme.display1.copyWith(
style: Theme.of(context).textTheme.headline4.copyWith(
fontSize: 16,
),
),
......
......@@ -17,6 +17,7 @@
import 'package:bloc/bloc.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
import '../../../matrix.dart';
......@@ -76,20 +77,30 @@ class ChatsBloc extends Bloc<ChatsEvent, ChatsState> {
final chat = Chat(
room: room,
name: await room.getDisplayName(),
isJustYou: room.members.count == 1,
latestMessage: latestEvent != null
? ChatMessage(
? await ChatMessage.create(
room,
latestEvent,
isMine: latestEvent.sender == _matrix.user,
isMe: (u) => u == _matrix.user,
)
: null,
latestMessageForSorting: latestEventForSorting != null
? ChatMessage(
room,
latestEventForSorting,
isMine: latestEventForSorting.sender == _matrix.user,
sender: await ChatMember.fromUser(
room,
latestEventForSorting.sender,
isYou: latestEventForSorting.sender == _matrix.user,
),
)
: null,
directMember: room.isDirect
? await ChatMember.fromUser(
room,
room.directUser,
isYou: room.directUser == _matrix.user,
)
: null,
);
......
......@@ -16,9 +16,9 @@
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:bloc/bloc.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
import '../../../../../matrix.dart';
import '../../../../../util/user.dart';
import 'event.dart';
export 'event.dart';
......@@ -31,7 +31,7 @@ class CreateGroupBloc extends Bloc<CreateGroupEvent, CreateGroupState> {
bool _isCreatingGroup = false;
final _members = List<User>();
final _members = List<ChatMember>();
String _groupName;
......@@ -44,13 +44,13 @@ class CreateGroupBloc extends Bloc<CreateGroupEvent, CreateGroupState> {
CreateGroupState get initialState => InitialCreateGroupState();
Stream<CreateGroupState> _loadUsers() async* {
final users = Set<User>();
final users = Set<ChatMember>();
// Load members of some rooms, in the future
// this'll be based on activity and what not
for (final room in await matrix.user.rooms.get(upTo: 10)) {
for (final user in await room.members.get(upTo: 20)) {
if (user != matrix.user) {
users.add(user);
users.add(await ChatMember.fromUser(room, user, isYou: false));
}
}
}
......@@ -58,7 +58,7 @@ class CreateGroupBloc extends Bloc<CreateGroupEvent, CreateGroupState> {
yield UserListUpdated(
users.toList(growable: false)
..sort(
(User a, User b) => a.displayName.compareTo(b.displayName),
(a, b) => a.name.compareTo(b.name),
),
members: _members,
);
......@@ -72,7 +72,7 @@ class CreateGroupBloc extends Bloc<CreateGroupEvent, CreateGroupState> {
final id = await matrix.user.rooms.create(
name: _groupName,
invitees: _members,
invitees: _members.map((m) => m.user),
);
// Await the next sync so the room has been processed
......@@ -97,12 +97,12 @@ class CreateGroupBloc extends Bloc<CreateGroupEvent, CreateGroupState> {
}
if (event is AddMember) {
_members.add(event.user);
_members.add(event.member);
yield MemberListUpdated(_members);
}
if (event is RemoveMember) {
_members.remove(event.user);
_members.remove(event.member);
yield MemberListUpdated(_members);
}
}
......
......@@ -21,9 +21,8 @@ import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/app.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/section/main/widgets/error.dart';
import 'package:pattle/src/section/main/widgets/user_avatar.dart';
import 'package:pattle/src/section/main/widgets/chat_member_avatar.dart';
import '../../../../../util/user.dart';
import 'bloc.dart';
class CreateGroupDetailsPage extends StatefulWidget {
......@@ -138,7 +137,7 @@ class _CreateGroupDetailsPageState extends State<CreateGroupDetailsPage> {
builder: (context, state) {
final children = List<Widget>();
for (final user in state?.members) {
for (final member in state?.members) {
children.add(
Column(
mainAxisSize: MainAxisSize.min,
......@@ -149,12 +148,12 @@ class _CreateGroupDetailsPageState extends State<CreateGroupDetailsPage> {
padding: EdgeInsets.all(8),
child: AspectRatio(
aspectRatio: 1 / 1,
child: UserAvatar(user: user, radius: 32),
child: ChatMemberAvatar(member: member, radius: 32),
),
),
),
Text(
user.getDisplayName(context),
member.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
......
import 'package:equatable/equatable.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
abstract class CreateGroupEvent extends Equatable {
@override
......@@ -18,15 +18,15 @@ class UpdateGroupName extends CreateGroupEvent {
}
class AddMember extends CreateGroupEvent {
final User user;
final ChatMember member;
AddMember(this.user);
AddMember(this.member);
}
class RemoveMember extends CreateGroupEvent {
final User user;
final ChatMember member;
RemoveMember(this.user);
RemoveMember(this.member);
}
class CreateGroup extends CreateGroupEvent {}
......@@ -19,7 +19,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pattle/src/app.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/section/main/widgets/user_item.dart';
import 'package:pattle/src/section/main/widgets/chat_member_tile.dart';
import '../../../../../matrix.dart';
import 'bloc.dart';
......@@ -82,8 +82,8 @@ class _CreateGroupMembersPageState extends State<CreateGroupMembersPage> {
return ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index) => UserItem(
user: users[index],
itemBuilder: (BuildContext context, int index) => ChatMemberTile(
member: users[index],
checkable: true,
onSelected: () => bloc.add(AddMember(users[index])),
onUnselected: () => bloc.add(RemoveMember(users[index])),
......
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
abstract class CreateGroupState extends Equatable {
final List<User> members;
final List<ChatMember> members;
CreateGroupState(this.members);
......@@ -16,11 +17,11 @@ class InitialCreateGroupState extends CreateGroupState {
}
class UserListUpdated extends CreateGroupState {
final List<User> users;
final List<ChatMember> users;
UserListUpdated(
this.users, {
@required List<User> members,
@required List<ChatMember> members,
}) : super(members);
@override
......@@ -28,14 +29,14 @@ class UserListUpdated extends CreateGroupState {
}
class MemberListUpdated extends CreateGroupState {
MemberListUpdated(List<User> members) : super(members);
MemberListUpdated(List<ChatMember> members) : super(members);
@override
List<Object> get props => [members];
}
class CreatingGroup extends CreateGroupState {
CreatingGroup({@required List<User> members}) : super(members);
CreatingGroup({@required List<ChatMember> members}) : super(members);
}
class CreatedGroup extends CreateGroupState {
......@@ -43,7 +44,7 @@ class CreatedGroup extends CreateGroupState {
CreatedGroup(
this.room, {
@required List<User> members,
@required List<ChatMember> members,
}) : super(members);
@override
......
......@@ -17,24 +17,34 @@
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:meta/meta.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
/// Chat overview used in the 'chats' page.
class Chat {
final Room room;
// TODO: When members are able to being accessed syncly, use member names
// for groups
final String name;
final Uri avatarUrl;
final ChatMessage latestMessage;
final ChatMessage latestMessageForSorting;
final bool isJustYou;
final ChatMember directMember;
Chat({
@required this.room,
@required this.name,
@required this.latestMessage,
@required this.latestMessageForSorting,
this.latestMessage,
this.latestMessageForSorting,
this.isJustYou = false,
});
this.directMember,
}) : name = room.name ??
(room.isDirect ? directMember.name : room.id.toString()),
avatarUrl = room.avatarUrl ??
(room.isDirect ? directMember.user.avatarUrl : room.avatarUrl);
}
......@@ -17,23 +17,22 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:mdi/mdi.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/chats/models/chat.dart';
import 'package:transparent_image/transparent_image.dart';
import '../../../../util/room.dart';
import '../../../../util/user.dart';
import '../../../../util/chat_member.dart';
import '../../../../util/url.dart';
class ChatAvatar extends StatelessWidget {
final Room room;
final Chat chat;
const ChatAvatar({Key key, this.room}) : super(key: key);
const ChatAvatar({Key key, this.chat}) : super(key: key);
@override
Widget build(BuildContext context) {
final avatarUrl = room.displayAvatarUrl;
final avatarUrl = chat.avatarUrl;
if (avatarUrl != null) {
return Container(
width: 48,
......@@ -51,8 +50,8 @@ class ChatAvatar extends StatelessWidget {
} else {
return CircleAvatar(
foregroundColor: Colors.white,
backgroundColor: room.isDirect
? room.directUser.getColor(context)
backgroundColor: chat.room.isDirect
? chat.directMember.color(context)
: LightColors.red[500],
radius: 24,
child: _icon(),
......@@ -61,9 +60,9 @@ class ChatAvatar extends StatelessWidget {
}
Icon _icon() {
if (room.isDirect) {
if (chat.room.isDirect) {
return Icon(Icons.person);
} else if (room.aliases != null && room.aliases.isNotEmpty) {
} else if (chat.room.aliases != null && chat.room.aliases.isNotEmpty) {
return Icon(Mdi.bullhorn);
} else {
return Icon(Icons.group);
......
......@@ -22,7 +22,7 @@ import 'package:pattle/src/section/main/widgets/chat_name.dart';
import 'package:pattle/src/util/date_format.dart';
import 'chat_avatar.dart';
import 'subtitle.dart';
import 'subtitle/subtitle.dart';
class ChatList extends StatefulWidget {
final List<Chat> chats;
......@@ -66,7 +66,7 @@ class ChatListState extends State<ChatList> {
),
Text(
time,
style: Theme.of(context).textTheme.subtitle.copyWith(
style: Theme.of(context).textTheme.subtitle2.copyWith(
fontWeight: FontWeight.normal,
color: Theme.of(context).textTheme.caption.color,
),
......@@ -77,7 +77,7 @@ class ChatListState extends State<ChatList> {
onTap: () {
Navigator.pushNamed(context, Routes.chats, arguments: chat);
},
leading: ChatAvatar(room: chat.room),
leading: ChatAvatar(chat: chat),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
subtitle: Subtitle.withContent(chat),
);
......
......@@ -19,7 +19,7 @@ import 'package:flutter/material.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/section/main/widgets/message_state.dart';
import 'subtitle.dart';
import '../subtitle.dart';
class ImageSubtitleContent extends Subtitle {
@override
......
......@@ -18,13 +18,13 @@
import 'package:flutter/material.dart';
import 'package:pattle/src/section/main/widgets/redacted.dart';
import 'subtitle.dart';
import '../subtitle.dart';
class RedactedSubtitleContent extends Subtitle {
@override
Widget build(BuildContext context) {
return Redacted(
event: Subtitle.of(context).chat.latestMessage.event,
redaction: Subtitle.of(context).chat.latestMessage.redaction,
);
}
}
......@@ -19,7 +19,7 @@ import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/widgets/message_state.dart';
import 'subtitle.dart';
import '../subtitle.dart';
class TextSubtitleContent extends StatelessWidget {
TextSubtitleContent({Key key}) : super(key: key);
......
......@@ -25,14 +25,13 @@ import 'package:pattle/src/section/main/chat/widgets/bubble/state/content/upgrad
import 'package:pattle/src/section/main/chats/models/chat.dart';
import 'package:provider/provider.dart';
import '../../../../matrix.dart';
import '../../../../util/user.dart';
import '../../../../../matrix.dart';
import 'image_subtitle.dart';
import 'redacted_subtitle.dart';
import 'text_subtitle.dart';
import 'typing_subtitle.dart';
import 'unsupported_subtitle.dart';
import 'content/image.dart';
import 'content/redacted.dart';
import 'content/text.dart';
import '../typing_content.dart';
import 'content/unsupported.dart';
class Subtitle extends StatelessWidget {
final Chat chat;
......@@ -46,7 +45,7 @@ class Subtitle extends StatelessWidget {
// TODO: typingUsers should not contain nulls
if (chat.room.isSomeoneElseTyping &&
!chat.room.typingUsers.any((u) => u == null)) {
content = TypingSubtitleContent();
content = TypingContent(chat: chat);
} else {
final event = chat.latestMessage?.event;
if (event == null) {
......@@ -82,7 +81,7 @@ class Subtitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: Theme.of(context).textTheme.body1.copyWith(
style: Theme.of(context).textTheme.bodyText2.copyWith(
color: Theme.of(context).textTheme.caption.color,
),
child: Provider<Subtitle>.value(
......@@ -127,7 +126,7 @@ class Sender extends StatelessWidget {
final message = Subtitle.of(context).chat.latestMessage;
return Text(
'${message.event.sender.displayName}: ',
'${message.sender.name}: ',
maxLines: 1,
style: TextStyle(fontWeight: FontWeight.bold),
);
......
......@@ -18,45 +18,60 @@
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/chats/models/chat.dart';
import '../../../../util/user.dart';
List<TextSpan> spanFor(
BuildContext context,
MemberChangeEvent event, {
TextStyle style = const TextStyle(fontWeight: FontWeight.bold),
}) {
final sender = TextSpan(
text: event.sender.displayName,
style: style,
);
final subject = TextSpan(
text: event.subject.displayName,
style: style,
);
var text;
if (event is DisplayNameChangeEvent) {
final oldName = TextSpan(
text: event.subject.nameOrDisplayId(name: event.oldSubjectName),
style: style,
);
class TypingContent extends StatelessWidget {
final Chat chat;
final newName = TextSpan(
text: event.subject.displayName,
style: style,
);
const TypingContent({Key key, @required this.chat}) : super(key: key);
List<TextSpan> _span(BuildContext context, Room room) {
if (room.isDirect) {
return l(context).typing;
}
if (room.typingUsers.length == 1) {
return l(context).isTyping(
TextSpan(
text: room.typingUsers.first.name,
),
);
}
text = l(context).changedTheirNameTo(oldName, newName);
} else if (event is JoinEvent) {
text = l(context).joined(subject);
} else if (event is LeaveEvent) {
text = l(context).left(subject);
} else if (event is InviteEvent) {
text = l(context).wasInvitedBy(subject, sender);
} else if (event is BanEvent) {
text = l(context).wasBannedBy(subject, sender);
if (room.typingUsers.length == 2) {
return l(context).areTyping(
TextSpan(
text: room.typingUsers.first.name,
),
TextSpan(
text: room.typingUsers[1].name,
),
);
}
return l(context).andMoreAreTyping(
TextSpan(
text: room.typingUsers.first.name,
),
TextSpan(
text: room.typingUsers[1].name,
),
);
}
return text;
@override
Widget build(BuildContext context) {
return RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: TextStyle(
color: redOnBackground(context),
fontWeight: FontWeight.bold,
),
children: _span(context, chat.room),
),
);
}
}
// Copyright (C) 2019 wilko
// Copyright (C) 2020 wilko
//
// This file is part of Pattle.
//
......@@ -15,42 +15,50 @@
// 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/widgets.dart';
import 'package:flutter/cupertino.dart';
import 'package:meta/meta.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/localizations.dart';
import '../../../../util/user.dart';
class ChatMember {
final User user;
List<TextSpan> typingSpan(BuildContext context, Room room) {
if (room.isDirect) {
return l(context).typing;
}
final String name;
final bool isYou;
final DisplayColor displayColor;
if (room.typingUsers.length == 1) {
return l(context).isTyping(
TextSpan(
text: room.typingUsers.first.displayName,
),
);
}
ChatMember(
this.user, {
@required this.name,
@required this.isYou,
}) : displayColor =
DisplayColor.values[user.id.hashCode % DisplayColor.values.length];
static Future<ChatMember> fromUser(
Room room,
User user, {
@required bool isYou,
}) async {
final currentName = (await room.members.getStateOf(user.id)).name;
if (room.typingUsers.length == 2) {
return l(context).areTyping(
TextSpan(
text: room.typingUsers.first.displayName,
),
TextSpan(
text: room.typingUsers[1].displayName,
),
return ChatMember(
user,
name: currentName ?? user.id.toString().split(':')[0],
isYou: isYou,
);
}
return l(context).andMoreAreTyping(
TextSpan(
text: room.typingUsers.first.displayName,
),
TextSpan(
text: room.typingUsers[1].displayName,
),
);
@override
bool operator ==(other) => user == other?.user;
@override
int get hashCode => user.hashCode;
}
enum DisplayColor {
greenYellow,
yellow,
blue,
purple,
green,
red,
}
......@@ -15,23 +15,32 @@
// 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/cupertino.dart';
import 'package:meta/meta.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
class ChatMessage {
final Room room;
final RoomEvent event;
final ChatMember sender;
final ChatMessage inReplyTo;
final bool isMine;
bool get isMine => sender.isYou;
/// Message that redacted this message, if any.
final ChatMessage redaction;
/// Subject of a member change state change, if any.
final ChatMember subject;
ChatMessage(
this.room,
this.event, {
@required this.sender,
this.inReplyTo,
@required this.isMine,
this.redaction,
this.subject,
});
@override
bool operator ==(other) {
if (other is ChatMessage) {
......@@ -44,6 +53,47 @@ class ChatMessage {
}
}
static Future<ChatMessage> create(
Room room,
RoomEvent event, {
ChatMessage inReplyTo,
@required bool Function(User) isMe,
}) async {
ChatMessage redactionMessage;
ChatMember subject;
if (event is RedactedEvent) {
redactionMessage = ChatMessage(
room,
event.redaction,
sender: await ChatMember.fromUser(
room,
event.redaction.sender,
isYou: isMe(event.redaction.sender),
),
);
} else if (event is MemberChangeEvent) {
subject = await ChatMember.fromUser(
room,
event.subject,
isYou: isMe(event.subject),
);
}
return ChatMessage(
room,
event,
sender: await ChatMember.fromUser(
room,
event.sender,
isYou: isMe(event.sender),
),
inReplyTo: inReplyTo,
redaction: redactionMessage,
subject: subject,
);
}
@override
int get hashCode =>
room.hashCode + event.hashCode + inReplyTo.hashCode + isMine.hashCode;
......
......@@ -18,10 +18,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/widgets/user_avatar.dart';
import 'package:pattle/src/section/main/widgets/chat_member_avatar.dart';
import '../../../matrix.dart';
import '../../../util/user.dart';
import '../../../util/local_user.dart';
import '../../../app.dart';
import 'bloc.dart';
......@@ -44,6 +44,7 @@ class _SettingsPageState extends State<SettingsPage> {
@override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<SettingsBloc>(context);
// TODO: Use ChatMember
final me = Matrix.of(context).user;
return Scaffold(
......@@ -68,8 +69,8 @@ class _SettingsPageState extends State<SettingsPage> {
children: <Widget>[
Hero(
tag: me.id,
child: UserAvatar(
user: me,
child: ChatMemberAvatar(
member: me.toChatMember(),
radius: 36,
),
),
......@@ -79,7 +80,7 @@ class _SettingsPageState extends State<SettingsPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
me.displayName,
me.name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
......
......@@ -19,9 +19,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/widgets/user_avatar.dart';
import 'package:pattle/src/section/main/widgets/chat_member_avatar.dart';
import '../../../matrix.dart';
import '../../../util/user.dart';
import '../../../util/local_user.dart';
import '../../../app.dart';
import 'bloc.dart';
......@@ -43,6 +44,7 @@ class ProfilePage extends StatefulWidget {
class _ProfilePageState extends State<ProfilePage> {
@override
Widget build(BuildContext context) {
// TODO: Use ChatMember
final me = Matrix.of(context).user;
return Scaffold(
......@@ -59,8 +61,8 @@ class _ProfilePageState extends State<ProfilePage> {
children: <Widget>[
Hero(
tag: me.id,
child: UserAvatar(
user: me,
child: ChatMemberAvatar(
member: me.toChatMember(),
radius: 96,
),
),
......@@ -84,7 +86,7 @@ class _ProfilePageState extends State<ProfilePage> {
color: redOnBackground(context),
),
title: Text(l(context).name),
subtitle: Text(me.displayName),
subtitle: Text(me.name),
trailing: Icon(Icons.edit),
onTap: () => Navigator.pushNamed(
context,
......
......@@ -30,7 +30,7 @@ class Header extends StatelessWidget {
text,
style: TextStyle(
color: redOnBackground(context),
fontSize: Theme.of(context).textTheme.body1.fontSize,
fontSize: Theme.of(context).textTheme.bodyText2.fontSize,
fontWeight: FontWeight.bold,
),
);
......
......@@ -17,30 +17,30 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/section/main/models/chat_member.dart';
import '../../../util/user.dart';
import '../../../util/chat_member.dart';
import '../../../util/url.dart';
class UserAvatar extends StatelessWidget {
final User user;
class ChatMemberAvatar extends StatelessWidget {
final ChatMember member;
final double radius;
UserAvatar({@required this.user, this.radius});
ChatMemberAvatar({@required this.member, this.radius});
@override
Widget build(BuildContext context) {
if (user.avatarUrl != null) {
if (member.user.avatarUrl !=