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

Fix member events not keeping to 1 line

parent 22373dd7
Pipeline #432 passed with stages
in 7 minutes and 15 seconds
A new version has been pushed to F-droid!
- Add ability to create group chats!
- Show chat creation events ('Wilko has created this group')!
- Show emote messages correctly!
- Handle display name changes!
Display names of messages will now be as they
were at time of sending.
- Don't show invite and join events in direct chats
This is only happens for the two initial users in the direct
chat. If someone invites someone else to the direct chat
(trough another client), the invitation will show up
in the timeline.
- Use the `timeout` parameter while syncing.
This means that receiving new messages should be
way quicker! (Thanks Mathieu!)
- Store messages retrieved remotely (thanks Mathieu!)
This means that scrolling up in a chat will be faster
now, because the messages are cached.
- Always show a date header above the oldest event
- Show replies correctly in chat overview
- Show sent state icon next to own message in chat overview
- Show newly joined rooms at the top in the chat overview
- Use a bit bolder font for chat names in overview
![Preview image 1](https://git.pattle.im/pattle/app/raw/v0.6.0/CHANGELOG/0.6.0-1.png)
![Preview image 2](https://git.pattle.im/pattle/app/raw/v0.6.0/CHANGELOG/0.6.0-2.png)
To install this release, add the following repo in F-droid:
https://fdroid.pattle.im/?fingerprint=E91F63CA6AE04F8E7EA53E52242EAF8779559209B8A342F152F9E7265E3EA729
And install 'Pattle'.
Or download the APK from the link.
If you stumble upon any issues,
[please report them](https://git.pattle.im/pattle/app/issues)!
You can now [login via GitHub and Gitlab.com](https://git.pattle.im/users/sign_in)
, so it's really easy to do!
Follow development here: [#app:pattle.im](https://matrix.to/#/#sdk:pattle.im)!
There is now also a room for the Matrix Dart SDK (which is
being developed for Pattle): [#sdk:pattle.im](https://matrix.to/#/#sdk:pattle.im).
If you would like to support me, you can now do so
via [Liberapay](https://liberapay.com/wilko/) and
[Patreon](https://www.patreon.com/pattle_app).
......@@ -29,13 +29,19 @@ import 'package:pattle/src/ui/start/phase/identity/username_page.dart';
import 'package:pattle/src/ui/start/phase/key/password_page.dart';
import 'package:pattle/src/ui/start/start_page.dart';
import 'package:pattle/src/ui/main/overview/create/group/create_group_members_page.dart';
import 'ui/main/overview/create/group/create_group_details_page.dart';
final routes = {
Routes.root: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.root),
builder: (context) => InitialPage()
),
Routes.chats: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.chats),
builder: (context) {
if (arguments is Room) {
return ChatPage(arguments);
......@@ -44,19 +50,32 @@ final routes = {
}
}
),
Routes.chatsNew: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.chatsNew),
builder: (context) => CreateGroupMembersPage()
),
Routes.chatsNewDetails: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.chatsNewDetails),
builder: (context) => CreateGroupDetailsPage()
),
Routes.image: (Object arguments) => MaterialPageRoute(
builder: (context) => ImagePage(arguments)
settings: RouteSettings(name: Routes.image),
builder: (context) => ImagePage(arguments)
),
Routes.start: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.start),
builder: (context) => StartPage()
),
Routes.startAdvanced: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.startAdvanced),
builder: (context) => AdvancedPage()
),
Routes.startUsername: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.startUsername),
builder: (context) => UsernamePage()
),
Routes.startPassword: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.startPassword),
builder: (context) => PasswordPage()
),
};
......@@ -73,6 +92,9 @@ class Routes {
static const startAdvanced = '/start/advanced';
static const startUsername = '/start/username';
static const startPassword = '/start/password';
static const chatsNew = '/chats/new';
static const chatsNewDetails = '/chats/new/details';
}
......
......@@ -75,7 +75,7 @@ class ChatBloc {
// Remember: 'previous' is actually next in time
RoomEvent previousEvent;
RoomEvent event;
await for(event in room.timeline.upTo(_eventCount)) {
await for(event in room.timeline.upTo(count: _eventCount)) {
var shouldIgnore = false;
// In direct chats, don't show the invite event between this user
......
......@@ -41,7 +41,7 @@ class ChatOverviewBloc {
final latestEvent = await room.timeline.all()
.firstWhere((event) => !ignoredEvents.contains(event.runtimeType), orElse: () => null);
var latestEventForSorting = await room.timeline.upTo(10)
var latestEventForSorting = await room.timeline.upTo(count: 10)
.firstWhere(
(event) =>
(event is! MemberChangeEvent
......
......@@ -61,7 +61,13 @@ class ChatOverviewPageState extends State<ChatOverviewPage> {
),
)
],
)
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).pushNamed(Routes.chatsNew);
},
child: Icon(Icons.chat),
),
);
}
......
// 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 'dart:collection';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/util/user.dart';
import 'package:rxdart/rxdart.dart';
import 'package:pattle/src/di.dart' as di;
final bloc = CreateGroupBloc();
class CreateGroupBloc {
final me = di.getLocalUser();
PublishSubject<List<User>> _userSubj = PublishSubject<List<User>>();
Stream<List<User>> get users => _userSubj.stream.distinct();
bool _isCreatingRoom = false;
PublishSubject<bool> _isCreatingRoomSubj = PublishSubject<bool>();
Stream<bool> get isCreatingRoom => _isCreatingRoomSubj.stream.distinct();
final usersToAdd = List<User>();
String groupName;
Future<void> loadMembers() async {
final users = HashSet<User>(
equals: (User a, User b) => a.isIdenticalTo(b),
hashCode: (User user) => user.id.hashCode
);
// Load members of some rooms, in the future
// this'll be based on activity and what not
await for (final room in me.rooms.upTo(count: 10)) {
await for (final user in room.members.upTo(count: 20)) {
if (!user.isIdenticalTo(me)) {
users.add(user);
}
}
}
_userSubj.add(
users.toList(growable: false)
..sort((User a, User b) => displayNameOf(a).compareTo(displayNameOf(b)))
);
}
Future<RoomId> createRoom() async {
if (!_isCreatingRoom) {
_isCreatingRoom = true;
_isCreatingRoomSubj.add(true);
return me.rooms.create(
name: groupName,
invitees: usersToAdd
).whenComplete(() {
_isCreatingRoom = false;
_isCreatingRoomSubj.add(false);
});
}
return null;
}
}
\ No newline at end of file
// 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/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/resources/localizations.dart';
import 'package:pattle/src/ui/util/matrix_image.dart';
import 'package:pattle/src/ui/util/user.dart';
import 'package:pattle/src/ui/main/overview/create/group/create_group_bloc.dart';
class CreateGroupDetailsPageState extends State<CreateGroupDetailsPage> {
@override
void initState() {
super.initState();
bloc.loadMembers();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(l(context).newGroup)
),
body: Column(
children: <Widget>[
ErrorBanner(),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: TextField(
onChanged: (text) {
bloc.groupName = text;
},
maxLines: 1,
decoration: InputDecoration(
labelText: l(context).groupName,
filled: true
),
),
)
)
],
),
Text(l(context).participants),
Expanded(
child: _buildUserList(context)
)
],
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// TODO: Navigate to newly created
await bloc.createRoom();
Navigator.of(context).popUntil(
(route) => route.settings.name == Routes.chats
);
},
child: StreamBuilder<bool>(
stream: bloc.isCreatingRoom,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
final isCreatingRoom = snapshot.data ?? false;
if (isCreatingRoom) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
)
);
} else {
return Icon(Icons.check);
}
},
),
),
);
}
Widget _buildUserList(BuildContext context) {
final children = List<Widget>();
for (final user in bloc.usersToAdd) {
children.add(Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Flexible(
child: Padding(
padding: EdgeInsets.all(8),
child: UserAvatar(
user: user,
radius: 32
)
),
),
Text(
displayNameOf(user),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
));
}
return GridView.count(
crossAxisCount: 4,
children: children
);
}
}
class CreateGroupDetailsPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => CreateGroupDetailsPageState();
}
\ No newline at end of file
// 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/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/resources/localizations.dart';
import 'package:pattle/src/ui/util/matrix_image.dart';
import 'package:pattle/src/ui/util/user.dart';
import 'package:pattle/src/ui/main/overview/create/group/create_group_bloc.dart';
class CreateGroupMembersPageState extends State<CreateGroupMembersPage> {
@override
void initState() {
super.initState();
bloc.loadMembers();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(l(context).newGroup)
),
body: Column(
children: <Widget>[
ErrorBanner(),
Expanded(
child: _buildUserList(context)
)
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).pushNamed(Routes.chatsNewDetails);
},
child: Icon(Icons.arrow_forward),
),
);
}
Widget _buildUserList(BuildContext context) {
return StreamBuilder<List<User>>(
stream: bloc.users,
builder: (BuildContext context, AsyncSnapshot<List<User>> snapshot) {
final users = snapshot.data;
if (users != null) {
return ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index)
=> _buildUser(context, users[index]),
);
} else {
return Container();
}
},
);
}
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: Theme.of(context).primaryColor,
),
)
)
),
)
],
);
}
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 {
@override
State<StatefulWidget> createState() => CreateGroupMembersPageState();
}
\ No newline at end of file
......@@ -32,6 +32,8 @@ class MemberSubtitle extends Subtitle {
@override
Widget build(BuildContext context) {
return RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: textStyle(context),
children: spanFor(context, event)
......
......@@ -23,13 +23,18 @@ import 'package:pattle/src/di.dart' as di;
final syncBloc = SyncBloc();
class SyncBloc {
var started = false;
LocalUser _user = di.getLocalUser();
ReplaySubject<SyncState> _syncSubj = ReplaySubject<SyncState>(maxSize: 1);
Observable<SyncState> get stream => _syncSubj.stream;
Future<void> start() async {
_user.sendAllUnsent();
_syncSubj.addStream(_user.sync());
if (!started) {
_user.sendAllUnsent();
_syncSubj.addStream(_user.sync());
started = true;
}
}
}
\ No newline at end of file
// 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/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/util/matrix_image.dart';
class UserAvatar extends StatelessWidget {
final User user;
final double radius;
UserAvatar({this.user, this.radius});
@override
Widget build(BuildContext context) {
if (user.avatarUrl != null) {
return CircleAvatar(
radius: radius,
backgroundImage: MatrixImage(
user.avatarUrl,
width: 64,
height: 64
),
);
} else {
return CircleAvatar(
radius: radius,
child: Icon(
Icons.person,
size: radius,
),
);
}
}
}
\ No newline at end of file
......@@ -40,6 +40,9 @@ class Strings {
final typeAMessage = 'Type a message';
final you = 'You';
final andOthers = 'and others';
final newGroup = 'New group';
final groupName = 'Group name';
final participants = 'Participants';
final connectionLost =
'Connection has been lost.\n'
......
......@@ -36,7 +36,7 @@ FutureOr<String> nameOf(BuildContext context, Room room) {
}
// TODO: Make upTo FutureOr
return room.members.upTo(6).toList().then((members) {
return room.members.upTo(count: 6).toList().then((members) {
var name = '';
if (members != null) {
if (members.length == 1) {
......
......@@ -215,14 +215,14 @@ packages:
name: matrix_sdk
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.0"
version: "0.12.0"
matrix_sdk_sqflite:
dependency: "direct main"
description:
name: matrix_sdk_sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0"
version: "0.8.0"
meta:
dependency: transitive
description:
......
......@@ -12,8 +12,8 @@ dependencies:
injector: ^1.0.6
matrix_sdk: ^0.10.0
matrix_sdk_sqflite: ^0.7.0
matrix_sdk: ^0.12.0
matrix_sdk_sqflite: ^0.8.0
rxdart: ^0.21.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment