Commit 7024e581 authored by Wilko Manger's avatar Wilko Manger

Implement paginating in chat timeline

parent 100115b4
......@@ -15,6 +15,8 @@
// 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:async';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/main/models/chat_item.dart';
import 'package:pattle/src/ui/main/sync_bloc.dart';
......@@ -51,90 +53,89 @@ class ChatBloc {
}
}
PublishSubject<List<ChatItem>> _itemSubj = PublishSubject<List<ChatItem>>();
Stream<List<ChatItem>> get items => _itemSubj.stream;
int _maxPagesCount;
int get maxPageCount => _maxPagesCount;
var isInitialLoad = true;
int _currentPage;
Future<void> startLoadingEvents() async {
await loadEvents();
PublishSubject<bool> _hasReachedEndSubj = PublishSubject<bool>();
Stream<bool> get hasReachedEnd => _hasReachedEndSubj.stream.distinct();
syncBloc.stream.listen((success) async => await loadEvents());
}
PublishSubject<bool> _shouldRefreshSubj = PublishSubject<bool>();
Stream<bool> get shouldRefresh => _shouldRefreshSubj.stream;
Future<void> requestMoreEvents() async {
if (!_isLoading) {
isLoading = true;
_eventCount += 30;
await loadEvents();
isLoading = false;
}
}
Future<void> loadEvents() async {
FutureOr<List<ChatItem>> getPage(int page) {
_currentPage = page;
final chatItems = List<ChatItem>();
// Remember: 'previous' is actually next in time
RoomEvent previousEvent;
RoomEvent event;
await for(event in room.timeline.get(upTo: _eventCount)) {
var shouldIgnore = false;
// In direct chats, don't show the invite event between this user
// and the direct user.
//
// Also in direct chats, don't show the join events between this user
// and the direct user.
if (room.isDirect) {
if (event is InviteEvent) {
final iInvitedYou = event.sender.isIdenticalTo(me)
&& event.content.subject.isIdenticalTo(room.directUser);
final youInvitedMe = event.sender.isIdenticalTo(room.directUser)
&& event.content.subject.isIdenticalTo(me);
shouldIgnore = iInvitedYou || youInvitedMe;
} else if (event is JoinEvent) {
final subject = event.content.subject;
shouldIgnore = subject.isIdenticalTo(me)
|| subject.isIdenticalTo(room.directUser);
List<ChatItem> toChatItems(Iterable<RoomEvent> events) {
// Remember: 'previous' is actually next in time
RoomEvent previousEvent;
RoomEvent event;
for(event in events) {
var shouldIgnore = false;
// In direct chats, don't show the invite event between this user
// and the direct user.
//
// Also in direct chats, don't show the join events between this user
// and the direct user.
if (room.isDirect) {
if (event is InviteEvent) {
final iInvitedYou = event.sender.isIdenticalTo(me)
&& event.content.subject.isIdenticalTo(room.directUser);
final youInvitedMe = event.sender.isIdenticalTo(room.directUser)
&& event.content.subject.isIdenticalTo(me);
shouldIgnore = iInvitedYou || youInvitedMe;
} else if (event is JoinEvent) {
final subject = event.content.subject;
shouldIgnore = subject.isIdenticalTo(me)
|| subject.isIdenticalTo(room.directUser);
}
}
}
shouldIgnore |=
event is JoinEvent && event is! DisplayNameChangeEvent
&& room.creator.isIdenticalTo(event.content.subject);
shouldIgnore |=
event is JoinEvent && event is! DisplayNameChangeEvent
&& room.creator.isIdenticalTo(event.content.subject);
// Don't show creation events in rooms that are replacements
shouldIgnore |= event is RoomCreationEvent && room.isReplacement;
// Don't show creation events in rooms that are replacements
shouldIgnore |= event is RoomCreationEvent && room.isReplacement;
if (ignoredEvents.contains(event.runtimeType) || shouldIgnore) {
continue;
}
if (ignoredEvents.contains(event.runtimeType) || shouldIgnore) {
continue;
}
// Insert DateHeader if there's a day difference
if (previousEvent != null && event != null
&& previousEvent.time.day != event.time.day) {
chatItems.add(DateItem(previousEvent.time));
}
// Insert DateHeader if there's a day difference
if (previousEvent != null && event != null
&& previousEvent.time.day != event.time.day) {
chatItems.add(DateItem(previousEvent.time));
chatItems.add(ChatEvent(event));
previousEvent = event;
}
chatItems.add(ChatEvent(event));
// Add date header above first event in room
if (event is RoomCreationEvent) {
chatItems.add(DateItem(event.time));
// If 10 items are already loaded, show them
if (chatItems.length >= 10 && isInitialLoad) {
isInitialLoad = false;
_itemSubj.add(List.of(chatItems));
_maxPagesCount = _currentPage + 1;
_hasReachedEndSubj.add(true);
}
previousEvent = event;
return chatItems.isNotEmpty ? chatItems : null;
}
// Add date header above first event in room
if (event is RoomCreationEvent) {
chatItems.add(DateItem(event.time));
}
final futureOrEvents = room.timeline.paginate(page: page);
_itemSubj.add(chatItems);
if (futureOrEvents is Iterable<RoomEvent>) {
return toChatItems(futureOrEvents);
} else {
final future = futureOrEvents as Future<Iterable<RoomEvent>>;
return future.then((events) => toChatItems(events));
}
}
Future<void> sendMessage(String text) async {
......@@ -143,7 +144,7 @@ class ChatBloc {
if (room is JoinedRoom && text.isNotEmpty) {
// Refresh the list every time the sent state changes.
await for (var sentState in room.send(TextMessage(body: text))) {
await loadEvents();
_shouldRefreshSubj.add(true);
}
}
}
......
......@@ -27,6 +27,7 @@ import 'package:pattle/src/ui/main/widgets/chat_name.dart';
import 'package:pattle/src/ui/main/widgets/error.dart';
import 'package:pattle/src/ui/resources/localizations.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/ui/util/future_or_builder.dart';
import 'package:pattle/src/ui/util/matrix_image.dart';
import 'package:pattle/src/di.dart' as di;
......@@ -45,26 +46,30 @@ class ChatPageState extends State<ChatPage> {
TextEditingController textController = TextEditingController();
int maxPageCount;
ChatPageState(this.room) {
bloc.room = room;
}
@override
void dispose() {
super.dispose();
}
@override
void initState() {
super.initState();
bloc.startLoadingEvents();
scrollController.addListener(() {
if (scrollController.offset >= scrollLoadRange
&& !scrollController.position.outOfRange) {
bloc.requestMoreEvents();
bloc.hasReachedEnd.listen((hasReachedEnd) {
if (hasReachedEnd) {
setState(() {
maxPageCount = bloc.maxPageCount;
});
}
});
}
@override
void dispose() {
super.dispose();
bloc.shouldRefresh.listen((shouldRefresh) => setState(() { }));
}
@override
......@@ -171,6 +176,9 @@ class ChatPageState extends State<ChatPage> {
onPressed: () {
bloc.sendMessage(textController.value.text);
textController.clear();
setState(() {
});
}
);
......@@ -263,57 +271,79 @@ class ChatPageState extends State<ChatPage> {
}
Widget _buildEventsList() {
return StreamBuilder<List<ChatItem>>(
stream: bloc.items,
builder: (BuildContext context, AsyncSnapshot<List<ChatItem>> snapshot) {
switch(snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return Center(child: PlatformCircularProgressIndicator());
case ConnectionState.active:
case ConnectionState.done:
var chatEvents = snapshot.data;
final widgets = List<Widget>();
var index = 0;
for (final item in chatEvents) {
if (item is ChatEvent) {
final event = item.event;
final isMine = event.sender.isIdenticalTo(me);
var previousItem, nextItem;
// Note: Because the items are reversed in the
// ListView.builder, the 'previous' event is actually the next
// one in the list.
if (index != chatEvents.length - 1) {
previousItem = chatEvents[index + 1];
return ListView.builder(
controller: scrollController,
reverse: true,
itemCount: maxPageCount,
itemBuilder: (BuildContext context, int index) {
return FutureOrBuilder<List<ChatItem>>(
futureOr: bloc.getPage(index),
builder: (BuildContext context, AsyncSnapshot<List<ChatItem>> snapshot) {
switch(snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return SizedBox(
height: MediaQuery.of(context).size.height * 2,
child: Align(
alignment: Alignment.bottomCenter,
child: PlatformCircularProgressIndicator()
),
);
case ConnectionState.active:
case ConnectionState.done:
print('building for $index (hasData: ${snapshot.hasData}');
if (!snapshot.hasData) {
return SizedBox(
height: MediaQuery.of(context).size.height * 2
);
}
if (index != 0) {
nextItem = chatEvents[index - 1];
final chatEvents = snapshot.data;
final widgets = List<Widget>();
var i = 0;
for (final item in chatEvents) {
if (item is ChatEvent) {
final event = item.event;
final isMine = event.sender.isIdenticalTo(me);
var previousItem, nextItem;
// Note: Because the items are reversed in the
// ListView.builder, the 'previous' event is actually the next
// one in the list.
if (i != chatEvents.length - 1) {
previousItem = chatEvents[i + 1];
}
if (i != 0) {
nextItem = chatEvents[i - 1];
}
widgets.add(Bubble.fromItem(
item: item,
previousItem: previousItem,
nextItem: nextItem,
isMine: isMine,
) ?? Container());
} else if (item is DateItem) {
widgets.add(DateHeader(item));
}
i++;
}
widgets.add(Bubble.fromItem(
item: item,
previousItem: previousItem,
nextItem: nextItem,
isMine: isMine,
) ?? Container());
} else if (item is DateItem) {
widgets.add(DateHeader(item));
}
index++;
return ListView(
reverse: true,
primary: false,
shrinkWrap: true,
children: widgets,
);
}
return ListView(
controller: scrollController,
reverse: true,
physics: const AlwaysScrollableScrollPhysics(),
children: widgets,
);
}
}
},
);
},
);
}
}
......
......@@ -60,7 +60,7 @@ class ImageBloc {
final imageMessageEvents = List<ImageMessageEvent>();
RoomEvent event;
await for (event in room.timeline.get(allowRemote: false)) {
for (event in await room.timeline.get(allowRemote: false)) {
if (event is ImageMessageEvent) {
imageMessageEvents.add(event);
}
......
......@@ -74,7 +74,8 @@ class TextBubble extends MessageBubble {
final repliedTo = event.room.timeline[event.content.inReplyToId];
return FutureOrBuilder<RoomEvent>(
futureOr: repliedTo,
builder: (BuildContext context, RoomEvent repliedTo) {
builder: (BuildContext context, AsyncSnapshot<RoomEvent> snapshot) {
final repliedTo = snapshot.data;
if (repliedTo != null && repliedTo is TextMessageEvent) {
return !isRepliedTo ? Padding(
padding: EdgeInsets.only(
......
......@@ -35,7 +35,7 @@ class ChatOverviewBloc {
final chats = List<ChatOverview>();
// Get all rooms and push them as a single list
await for(Room room in _user.rooms.get()) {
for(Room room in await _user.rooms.get()) {
// Don't show rooms that have been upgraded
if (room.isUpgraded) {
continue;
......@@ -44,19 +44,19 @@ class ChatOverviewBloc {
final ignoredEvents = ignoredEventsOf(room, isOverview: true);
// TODO: Add optional filter argument to up to call
final latestEvent = await room.timeline.get(
final latestEvent = (await room.timeline.get(
upTo: 10,
allowRemote: false
)
))
.firstWhere(
(event) => !ignoredEvents.contains(event.runtimeType),
orElse: () => null
);
var latestEventForSorting = await room.timeline.get(
var latestEventForSorting = (await room.timeline.get(
upTo: 10,
allowRemote: false
)
))
.firstWhere(
(event) =>
(event is! MemberChangeEvent
......
......@@ -49,8 +49,8 @@ class CreateGroupBloc {
);
// Load members of some rooms, in the future
// this'll be based on activity and what not
await for (final room in me.rooms.get(upTo: 10)) {
await for (final user in room.members.get(upTo: 20)) {
for (final room in await me.rooms.get(upTo: 10)) {
for (final user in await room.members.get(upTo: 20)) {
if (!user.isIdenticalTo(me)) {
users.add(user);
}
......
......@@ -45,12 +45,17 @@ class ChatName extends StatelessWidget {
Widget build(BuildContext context) {
return FutureOrBuilder<String>(
futureOr: nameOf(context, room),
builder: (BuildContext context, String name) {
return Text(name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: _textStyle()
);
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
final name = snapshot.data;
return Text(name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: _textStyle()
);
}
return Container();
},
);
}
......
......@@ -19,14 +19,12 @@ import 'dart:async';
import 'package:flutter/widgets.dart';
typedef Builder<T> = Widget Function(BuildContext context, T data);
/// Builds immediately if the `FutureOr` is the `T`,
/// other wise build a `FutureBuilder`.
class FutureOrBuilder<T> extends StatelessWidget {
final FutureOr<T> futureOr;
final Builder<T> builder;
final AsyncWidgetBuilder<T> builder;
const FutureOrBuilder({
Key key,
......@@ -37,17 +35,12 @@ class FutureOrBuilder<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (futureOr is T) {
return builder(context, futureOr);
return builder(context, AsyncSnapshot.withData(ConnectionState.done, futureOr));
} else {
return FutureBuilder<T>(
future: futureOr,
builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return builder(context, snapshot.data);
}
return Container();
});
builder: builder
);
}
}
......
......@@ -36,8 +36,9 @@ FutureOr<String> nameOf(BuildContext context, Room room) {
return displayNameOf(room.directUser);
}
// TODO: Make upTo FutureOr
return room.members.get(upTo: 6).toList().then((members) {
final futureOrMembers = room.members.get(upTo: 6);
String calculateName(Iterable<User> members) {
var name = '';
if (members != null) {
if (members.length == 1) {
......@@ -45,8 +46,8 @@ FutureOr<String> nameOf(BuildContext context, Room room) {
// TODO: Check for aliases (public chats)
} else {
final nonMeMembers = members = members
.where((user) => !user.isIdenticalTo(di.getLocalUser()))
.toList(growable: false);
.where((user) => !user.isIdenticalTo(di.getLocalUser()))
.toList(growable: false);
var i = 0;
for (User member in nonMeMembers) {
......@@ -69,7 +70,13 @@ FutureOr<String> nameOf(BuildContext context, Room room) {
}
return name.isNotEmpty ? name : room.id.toString();
});
}
if (futureOrMembers is Future<Iterable<User>>) {
return futureOrMembers.then(calculateName);
} else {
return calculateName(futureOrMembers);
}
}
List<Type> ignoredEventsOf(Room room, {@required bool isOverview}) {
......
......@@ -49,7 +49,7 @@ packages:
name: chopper
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
version: "2.4.2"
collection:
dependency: transitive
description:
......@@ -213,14 +213,14 @@ packages:
name: matrix_sdk
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.3"
version: "0.18.2"
matrix_sdk_sqflite:
dependency: "direct main"
description:
name: matrix_sdk_sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.0"
version: "0.14.0"
meta:
dependency: transitive
description:
......
......@@ -12,8 +12,8 @@ dependencies:
injector: ^1.0.6
matrix_sdk: ^0.17.3
matrix_sdk_sqflite: ^0.13.0
matrix_sdk: ^0.18.2
matrix_sdk_sqflite: ^0.14.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