Commit 11cb66b9 authored by Wilko Manger's avatar Wilko Manger

Add members list to chat settings page

parent a64c3105
......@@ -70,23 +70,23 @@ class ChatBloc {
// and the direct user.
if (room.isDirect) {
if (event is InviteEvent) {
final iInvitedYou = event.sender.isIdenticalTo(me)
&& event.content.subject.isIdenticalTo(room.directUser);
final iInvitedYou = event.sender == me
&& event.content.subject == room.directUser;
final youInvitedMe = event.sender.isIdenticalTo(room.directUser)
&& event.content.subject.isIdenticalTo(me);
final youInvitedMe = event.sender == room.directUser
&& event.content.subject == me;
shouldIgnore = iInvitedYou || youInvitedMe;
} else if (event is JoinEvent) {
final subject = event.content.subject;
shouldIgnore = subject.isIdenticalTo(me)
|| subject.isIdenticalTo(room.directUser);
shouldIgnore = subject == me
|| subject == room.directUser;
}
}
shouldIgnore |=
event is JoinEvent && event is! DisplayNameChangeEvent
&& room.creator.isIdenticalTo(event.content.subject);
&& room.creator == event.content.subject;
// Don't show creation events in rooms that are replacements
shouldIgnore |= event is RoomCreationEvent && room.isReplacement;
......
......@@ -318,7 +318,7 @@ class ChatPageState extends State<ChatPage> {
for (final item in chatEvents) {
if (item is ChatEvent) {
final event = item.event;
final isMine = event.sender.isIdenticalTo(me);
final isMine = event.sender == me;
var previousItem, nextItem;
// Note: Because the items are reversed in the
......
......@@ -30,4 +30,33 @@ class ChatSettingsBloc {
final Room room;
ChatSettingsBloc(this.room);
FutureOr<List<User>> getMembers({bool all = false}) {
FutureOr<List<User>> filter(Iterable<User> members) {
final list = members.toList();
final futureOrMe = room.members[di.getLocalUser().id];
FutureOr<List<User>> reorder(User me) {
list.remove(me);
list.insert(0, me);
return list;
}
if (futureOrMe is Future<User>) {
return futureOrMe.then(reorder);
} else {
return reorder(futureOrMe);
}
}
final futureOrMembers = room.members.get(upTo: !all ? 6 : room.members.count);
if (futureOrMembers is Future<Iterable<User>>) {
return futureOrMembers.then(filter);
} else {
return filter(futureOrMembers);
}
}
}
\ No newline at end of file
......@@ -22,11 +22,14 @@ import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/main/chat/chat_bloc.dart';
import 'package:pattle/src/ui/main/chat/settings/chat_settings_bloc.dart';
import 'package:pattle/src/ui/main/widgets/chat_name.dart';
import 'package:pattle/src/ui/main/widgets/user_item.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/di.dart' as di;
import 'package:pattle/src/ui/util/future_or_builder.dart';
import 'package:pattle/src/ui/util/matrix_image.dart';
import 'package:pattle/src/ui/util/room.dart';
import 'package:pattle/src/ui/util/user.dart';
class ChatSettingsPageState extends State<ChatSettingsPage> {
......@@ -35,6 +38,8 @@ class ChatSettingsPageState extends State<ChatSettingsPage> {
final ChatSettingsBloc bloc;
final Room room;
bool previewMembers;
ChatSettingsPageState(this.room) : bloc = ChatSettingsBloc(room);
@override
......@@ -45,6 +50,7 @@ class ChatSettingsPageState extends State<ChatSettingsPage> {
@override
void initState() {
super.initState();
previewMembers = true;
}
@override
......@@ -72,9 +78,15 @@ class ChatSettingsPageState extends State<ChatSettingsPage> {
),
];
},
body: Column(
children: <Widget>[
_buildDescription(),
body: CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate.fixed([
_buildDescription(),
SizedBox(height: 16),
_buildMembers()
]),
)
],
)
),
......@@ -82,7 +94,7 @@ class ChatSettingsPageState extends State<ChatSettingsPage> {
}
Widget _buildDescription() {
if (room.topic == null) {
if (room.isDirect) {
return Container(height: 0);
}
......@@ -105,7 +117,12 @@ class ChatSettingsPageState extends State<ChatSettingsPage> {
),
),
SizedBox(height: 4),
Text(room.topic),
Text(room.topic ?? 'None',
style: TextStyle(
fontStyle: room.topic == null
? FontStyle.italic : FontStyle.normal
),
),
],
),
),
......@@ -115,6 +132,94 @@ class ChatSettingsPageState extends State<ChatSettingsPage> {
);
}
Widget _buildMembers() {
if (room.isDirect) {
return Container(height: 0);
}
return Row(
children: <Widget>[
Expanded(
child: Material(
elevation: 4,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 16, top: 16),
child: Text(
'${bloc.room.members.count} participants',
style: TextStyle(
color: LightColors.red,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(height: 4),
FutureOrBuilder<Iterable<User>>(
futureOr: bloc.getMembers(all: !previewMembers),
builder: (
BuildContext context,
AsyncSnapshot<Iterable<User>> snapshot,
) {
if (!snapshot.hasData) {
return Container(height: 0);
}
final members = snapshot.data.toList(growable: false);
final isWaiting = snapshot.connectionState == ConnectionState.waiting;
bool allShown = members.length == bloc.room.members.count;
print(isWaiting);
return MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: ListView.builder(
primary: false,
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: (previewMembers && !allShown) || isWaiting
? members.length + 1
: members.length,
itemBuilder: (BuildContext context, int index) {
print('build: $index');
// Item after all members
if (index == members.length) {
return _buildShowMoreItem(context, members.length, isWaiting);
}
return UserItem(
user: members[index],
);
},
),
);
}
),
],
),
),
),
],
);
}
Widget _buildShowMoreItem(BuildContext context, int count, bool isWaiting) {
return ListTile(
leading: Icon(Icons.keyboard_arrow_down, size: 32),
title: Text('${bloc.room.members.count - count} more'),
subtitle: isWaiting ? LinearProgressIndicator() : null,
onTap: () => setState(() {
previewMembers = false;
}),
);
}
}
class ChatSettingsPage extends StatefulWidget {
......
......@@ -31,7 +31,9 @@ List<TextSpan> typingSpan(BuildContext context, Room room) {
text: displayNameOf(room.typingUsers.first),
),
);
} else if (room.typingUsers.length == 2) {
}
if (room.typingUsers.length == 2) {
return l(context).areTyping(
TextSpan(
text: displayNameOf(room.typingUsers.first),
......@@ -40,14 +42,14 @@ List<TextSpan> typingSpan(BuildContext context, Room room) {
text: displayNameOf(room.typingUsers[1]),
),
);
} else {
return l(context).andMoreAreTyping(
TextSpan(
text: displayNameOf(room.typingUsers.first),
),
TextSpan(
text: displayNameOf(room.typingUsers[1]),
),
);
}
return l(context).andMoreAreTyping(
TextSpan(
text: displayNameOf(room.typingUsers.first),
),
TextSpan(
text: displayNameOf(room.typingUsers[1]),
),
);
}
\ No newline at end of file
......@@ -113,7 +113,7 @@ abstract class MessageBubble extends Bubble {
@protected
Widget buildSender(BuildContext context, {Color color}) {
if ((isStartOfGroup || (isRepliedTo && !isMine)) && !event.room.isDirect) {
return Text(displayNameOf(event.sender),
return Text(displayNameOf(event.sender, context),
style: senderTextStyle(context, color: color)
);
} else {
......@@ -142,7 +142,7 @@ abstract class MessageBubble extends Bubble {
final previousHasSameSender
= previousEvent != null
&& displayNameOf(previousEvent.sender) == displayNameOf(event.sender)
&& previousEvent.sender.isIdenticalTo(event.sender);
&& previousEvent.sender == event.sender;
if (!previousHasSameSender) {
_isStartOfGroup = true;
......@@ -187,7 +187,7 @@ abstract class MessageBubble extends Bubble {
final nextHasSameSender
= nextEvent != null
&& displayNameOf(nextEvent.sender) == displayNameOf(event.sender)
&& nextEvent.sender.isIdenticalTo(event.sender);
&& nextEvent.sender == event.sender;
if (!nextHasSameSender) {
_isEndOfGroup = true;
......
......@@ -20,6 +20,7 @@ import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/main/models/chat_item.dart';
import 'package:pattle/src/ui/resources/localizations.dart';
import 'package:pattle/src/ui/util/future_or_builder.dart';
import 'package:pattle/src/ui/util/user.dart';
import 'state_bubble.dart';
......@@ -44,6 +45,16 @@ class CreationBubble extends StateBubble {
isMine: isMine
);
@protected
Widget buildContent(BuildContext context) {
return FutureOrBuilder<User>(
futureOr: event.room.creator,
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
return super.buildContent(context);
},
);
}
@protected
@override
......
......@@ -56,7 +56,7 @@ class TopicBubble extends StateBubble {
List<TextSpan> buildContentSpans(BuildContext context) =>
l(context).changedDescriptionTapToView(
TextSpan(
text: displayNameOf(event.room.creator),
text: displayNameOf(event.sender),
style: defaultEmphasisTextStyle
)
);
......
......@@ -88,7 +88,7 @@ class TextBubble extends MessageBubble {
child: Bubble.asReply(
reply: event,
replyTo: repliedTo,
isMine: repliedTo.sender.isIdenticalTo(me)
isMine: repliedTo.sender == me
),
) : Container();
} else {
......@@ -124,7 +124,7 @@ class TextBubble extends MessageBubble {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(displayNameOf(event.sender) + ' ',
Text(displayNameOf(event.sender, context) + ' ',
style: senderTextStyle(context,
color: isMine ? Colors.white : null
),
......@@ -139,7 +139,7 @@ class TextBubble extends MessageBubble {
@protected
Widget buildMine(BuildContext context) {
final needsBorder = isRepliedTo && reply.sender.isIdenticalTo(me);
final needsBorder = isRepliedTo && reply.sender == me;
return PlatformInkWell(
onTap: () { },
......@@ -170,7 +170,7 @@ class TextBubble extends MessageBubble {
@protected
Widget buildTheirs(BuildContext context) {
final needsBorder = isRepliedTo && !reply.sender.isIdenticalTo(me);
final needsBorder = isRepliedTo && reply.sender != me;
// Don't show sender above emotes
final sender = event is! EmoteMessageEvent
......
......@@ -43,15 +43,12 @@ class CreateGroupBloc {
JoinedRoom get createdRoom => _createdRoom;
Future<void> loadMembers() async {
final users = HashSet<User>(
equals: (User a, User b) => a.isIdenticalTo(b),
hashCode: (User user) => user.id.hashCode
);
final users = Set<User>();
// Load members of some rooms, in the future
// this'll be based on activity and what not
for (final room in await me.rooms.get(upTo: 10)) {
for (final user in await room.members.get(upTo: 20)) {
if (!user.isIdenticalTo(me)) {
if (user != me) {
users.add(user);
}
}
......
......@@ -155,7 +155,7 @@ class CreateGroupDetailsPageState extends State<CreateGroupDetailsPage> {
),
),
Text(
displayNameOf(user),
displayNameOf(user, context),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
......
......@@ -21,6 +21,7 @@ import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/app.dart';
import 'package:pattle/src/ui/main/widgets/error.dart';
import 'package:pattle/src/ui/main/widgets/user_avatar.dart';
import 'package:pattle/src/ui/main/widgets/user_item.dart';
import 'package:pattle/src/ui/resources/localizations.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/ui/util/matrix_image.dart';
......@@ -92,8 +93,13 @@ class CreateGroupMembersPageState extends State<CreateGroupMembersPage> {
if (users != null) {
return ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index)
=> _buildUser(context, users[index]),
itemBuilder: (BuildContext context, int index) =>
UserItem(
user: users[index],
checkable: true,
onSelected: () => bloc.usersToAdd.add(users[index]),
onUnselected: () => bloc.usersToAdd.remove(users[index]),
),
);
} else {
return Container();
......@@ -101,60 +107,6 @@ class CreateGroupMembersPageState extends State<CreateGroupMembersPage> {
},
);
}
Widget _buildUser(BuildContext context, User user) {
final avatarSize = 42.0;
Widget avatar = UserAvatar(
user: user,
radius: avatarSize * 0.5,
);
// TODO: Add checkmark animation
if (bloc.usersToAdd.contains(user)) {
avatar = Stack(
overflow: Overflow.visible,
children: <Widget>[
avatar,
SizedBox(
width: avatarSize,
height: avatarSize,
child: Align(
alignment: Alignment(1.5, 1.5),
child: ClipOval(
child: Container(
color: Colors.white,
child: Icon(
Icons.check_circle,
color: LightColors.red,
),
)
)
),
)
],
);
}
return ListTile(
leading: avatar,
title: Text(
displayNameOf(user),
style: TextStyle(
fontWeight: FontWeight.w600
),
),
onTap: () {
setState(() {
if (bloc.usersToAdd.contains(user)) {
bloc.usersToAdd.remove(user);
} else {
bloc.usersToAdd.add(user);
}
});
},
);
}
}
class CreateGroupMembersPage extends StatefulWidget {
......
......@@ -39,10 +39,10 @@ abstract class Subtitle extends StatelessWidget {
final bool isMine;
Subtitle(this.event)
: isMine = event?.sender?.isIdenticalTo(di.getLocalUser()),
: isMine = event?.sender == di.getLocalUser(),
senderName =
event != null
&& !event.sender.isIdenticalTo(di.getLocalUser())
&& event.sender != di.getLocalUser()
&& !event.room.isDirect
? '${displayNameOf(event.sender)}: '
: '';
......
......@@ -32,7 +32,7 @@ class TextSubtitle extends Subtitle {
Widget build(BuildContext context) {
final sender = senderSpan(context,
sender: event is EmoteMessageEvent
? displayNameOf(event.sender) + ' ' : null
? displayNameOf(event.sender, context) + ' ' : null
);
if (event.content.inReplyToId == null) {
return Row(
......
......@@ -39,7 +39,7 @@ class Redacted extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<TextSpan> text;
if (event.redaction.sender.isIdenticalTo(di.getLocalUser())) {
if (event.redaction.sender == di.getLocalUser()) {
text = [TextSpan(text: ' ${l(context).youDeletedThisMessage}')];
} else {
text = l(context).hasDeletedThisMessage(
......
// Copyright (C) 2019 wilko
//
// 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/ui/util/user.dart';
import 'user_avatar.dart';
class UserItem extends StatefulWidget {
final User user;
final bool checkable;
final bool checked;
final VoidCallback onSelected;
final VoidCallback onUnselected;
const UserItem({
Key key,
@required this.user,
this.checkable = false,
this.checked = false,
this.onSelected,
this.onUnselected,
}) : super(key: key);
@override
State<StatefulWidget> createState() => UserItemState();
}
class UserItemState extends State<UserItem> {
bool checked;
@override
void initState() {
super.initState();
checked = widget.checked;
}
@override
Widget build(BuildContext context) {
final avatarSize = 42.0;
Widget avatar = UserAvatar(
user: widget.user,
radius: avatarSize * 0.5,
);
// TODO: Add checkmark animation
if (widget.checkable && checked) {
avatar = Stack(
overflow: Overflow.visible,
children: <Widget>[
avatar,
SizedBox(
width: avatarSize,
height: avatarSize,
child: Align(
alignment: Alignment(1.5, 1.5),
child: ClipOval(
child: Container(
color: Colors.white,
child: Icon(
Icons.check_circle,
color: LightColors.red,
),
),
),
),
)
],
);
}
return ListTile(
leading: avatar,
title: Text(
displayNameOf(widget.user, context),
style: TextStyle(
fontWeight: FontWeight.w600,
),
),
onTap: widget.checkable
? () {
setState(() {
checked = !checked;
if (checked) {
widget.onSelected();
} else {
widget.onUnselected();
}
});
}
: null,
);
}
}
\ No newline at end of file
......@@ -33,7 +33,7 @@ FutureOr<String> nameOf(BuildContext context, Room room) {
}
if (room.isDirect) {
return displayNameOf(room.directUser);
return displayNameOf(room.directUser, context);
}
String calculateName(Iterable<User> members) {
......@@ -44,7 +44,7 @@ FutureOr<String> nameOf(BuildContext context, Room room) {
// TODO: Check for aliases (public chats)
} else {
final nonMeMembers = members
.where((user) => !user.isIdenticalTo(di.getLocalUser()))
.where((user) => user != di.getLocalUser())
.toList(growable: false);
var i = 0;
......@@ -54,7 +54,7 @@ FutureOr<String> nameOf(BuildContext context, Room room) {
break;
}
name += displayNameOf(member);
name += displayNameOf(member, context);
if (i != nonMeMembers.length - 1) {
name += ', ';
......
......@@ -17,9 +17,11 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:meta/meta.dart';
import 'package:pattle/src/ui/resources/localizations.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/di.dart' as di;
const _limit = 28;
String _limited(String name) {
......@@ -30,12 +32,20 @@ String _limited(String name) {
}
}
String displayNameOf(User user)
=> _limited(user.name) ?? user.id.toString().split(':')[0];
/// Get the proper display name for [user].
///
/// If [context] is provided, the local user will be 'You' instead
/// of their actual display name.
///
/// This is however not always desired, mostly only when showing the
/// display name to the end user.
String displayNameOf(User user, [BuildContext context]) =>
context != null && user == di.getLocalUser()
? l(context).you
: _limited(user?.name) ?? user.id.toString().split(':')[0];
String displayNameOrId(UserId id, String name)
=> _limited(name) ?? id.toString().split(':')[0];
String displayNameOrId(UserId id, String name) =>
_limited(name) ?? id.toString().split(':')[0];
Color colorOf(User user) => LightColors.userColors[
user.id.hashCode % LightColors.userColors.length
];
\ No newline at end of file
Color colorOf(User user) =>
LightColors.userColors[user.id.hashCode % LightColors.userColors.length];