Skip to content

Commit

Permalink
msglist: Add MarkAsRead widget
Browse files Browse the repository at this point in the history
  • Loading branch information
sirpengi committed Oct 26, 2023
1 parent 78069d2 commit 1cdb83a
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 2 deletions.
16 changes: 16 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,22 @@
"@serverUrlValidationErrorUnsupportedScheme": {
"description": "Error message when URL has an unsupported scheme."
},
"markAsReadAll": "Mark all as read",
"@markAsReadAll": {
"description": "Button label to mark all messages as read"
},
"markAsReadStream": "Mark stream as read",
"@markAsReadStream": {
"description": "Button label to mark messages in current stream as read"
},
"markAsReadTopic": "Mark topic as read",
"@markAsReadTopic": {
"description": "Button label to mark messages in current topic as read"
},
"markAsReadDm": "Mark conversation as read",
"@markAsReadDm": {
"description": "Button label to mark messages in current DM conversation as read"
},
"userRoleOwner": "Owner",
"@userRoleOwner": {
"description": "Label for UserRole.owner"
Expand Down
32 changes: 32 additions & 0 deletions lib/model/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class MessageListHistoryStartItem extends MessageListItem {
const MessageListHistoryStartItem();
}

/// Allows user to mark unread messages in this narrow as read.
class MessageListMarkReadItem extends MessageListItem {
const MessageListMarkReadItem();
}

/// The sequence of messages in a message list, and how to display them.
///
/// This comprises much of the guts of [MessageListView].
Expand Down Expand Up @@ -117,6 +122,7 @@ mixin _MessageSequence {
switch (item.direction) {
case MessageListDirection.older: return -1;
}
case MessageListMarkReadItem(): return 1;
case MessageListRecipientHeaderItem(:var message):
return (message.id <= messageId) ? -1 : 1;
case MessageListMessageItem(:var message): return message.id.compareTo(messageId);
Expand Down Expand Up @@ -145,7 +151,9 @@ mixin _MessageSequence {
messages.add(message);
contents.add(parseContent(message.content));
assert(contents.length == messages.length);
_clearEndMarkers();
_processMessage(messages.length - 1);
_updateEndMarkers();
}

void _insertAllMessages(int index, Iterable<Message> toInsert) {
Expand Down Expand Up @@ -196,6 +204,10 @@ mixin _MessageSequence {
showSender: !canShareSender, isLastInBlock: true));
}

void _clearEndMarkers() {
if (items.lastOrNull is MessageListMarkReadItem) items.removeLast();
}

/// Update [items] to include markers at start and end as appropriate.
void _updateEndMarkers() {
assert(!(haveOldest && fetchingOlder));
Expand All @@ -215,6 +227,25 @@ mixin _MessageSequence {
case (_, true): items.removeFirst();
case (_, _ ): break;
}

// TODO(#253): replace after #304 is merged
const hasUnreadMessages = true;
// final hasUnreadMessages = messages.any(
// (message) => !message.flags.contains(MessageFlag.read));
final endMarker = switch(hasUnreadMessages) {
true => const MessageListMarkReadItem(),
_ => null,
};
final hasEndMarker = switch (items.lastOrNull) {
MessageListMarkReadItem() => true,
_ => false,
};
switch ((endMarker != null, hasEndMarker)) {
case (true, true): items[items.length - 1] = endMarker!;
case (true, _ ): items.addLast(endMarker!);
case (_, true): items.removeLast();
case (_, _ ): break;
}
}

/// Recompute [items] from scratch, based on [messages], [contents], and flags.
Expand Down Expand Up @@ -446,6 +477,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
return;
}

_updateEndMarkers();
notifyListeners();
}

Expand Down
73 changes: 73 additions & 0 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import 'dart:math';

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
import 'package:intl/intl.dart';

import '../api/model/model.dart';
import '../api/route/messages.dart';
import '../model/message_list.dart';
import '../model/narrow.dart';
import '../model/store.dart';
Expand Down Expand Up @@ -301,6 +303,8 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: CircularProgressIndicator())); // TODO perhaps a different indicator
case MessageListMarkReadItem():
return MarkReadWidget(model: model!);
case MessageListRecipientHeaderItem():
final header = RecipientHeader(message: data.message);
return StickyHeaderItem(allowOverflow: true,
Expand Down Expand Up @@ -348,6 +352,75 @@ class ScrollToBottomButton extends StatelessWidget {
}
}

class MarkReadWidget extends StatelessWidget {
const MarkReadWidget({super.key, required this.model});

final MessageListView model;

@override
Widget build(BuildContext context) {
final zulipLocalizations = ZulipLocalizations.of(context);
final message = switch (model.narrow) {
AllMessagesNarrow() => zulipLocalizations.markAsReadAll,
StreamNarrow() => zulipLocalizations.markAsReadStream,
TopicNarrow() => zulipLocalizations.markAsReadTopic,
DmNarrow() => zulipLocalizations.markAsReadDm,
};
final hasUnreads = model.store.unreads.countInNarrow(model.narrow) > 0;
return AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: hasUnreads ? CrossFadeState.showFirst : CrossFadeState.showSecond,
secondChild: const SizedBox.shrink(),
firstChild: SizedBox(width: double.infinity, child: ColoredBox(
color: const HSLColor.fromAHSL(1, 204, 0.58, 0.92).toColor(),
child: Padding(
padding: const EdgeInsets.all(10),
child: FilledButton.icon(
style: FilledButton.styleFrom(
padding: const EdgeInsets.all(10),
textStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.w200),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
),
onPressed: () async {
if (!context.mounted) return;
final narrow = model.narrow;
final connection = model.store.connection;
if (connection.zulipFeatureLevel! >= 155) {
await updateMessageFlagsForNarrow(connection,
anchor: AnchorCode.firstUnread,
includeAnchor: true,
numBefore: 0,
numAfter: 5000, // based on server's MAX_MESSAGES_PER_UPDATE
narrow: narrow.apiEncode(),
op: UpdateMessageFlagsOp.add,
flag: MessageFlag.read);
} else {
switch (narrow) {
case AllMessagesNarrow():
await markAllAsRead(connection);
case StreamNarrow():
await markStreamAsRead(connection,
streamId: narrow.streamId);
case TopicNarrow():
await markTopicAsRead(connection,
streamId: narrow.streamId, topicName: narrow.topic);
case DmNarrow():
final unreadDms = model.store.unreads.dms[narrow];
if (unreadDms == null) {
return;
}
await updateMessageFlags(connection,
messages: unreadDms.toList(),
op: UpdateMessageFlagsOp.add,
flag: MessageFlag.read);
}
}
},
icon: const Icon(Icons.playlist_add_check),
label: Text(message))))));
}
}

class RecipientHeader extends StatelessWidget {
const RecipientHeader({super.key, required this.message});

Expand Down
4 changes: 4 additions & 0 deletions test/model/message_list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@ void main() async {
it()..isA<MessageListMessageItem>().showSender.isTrue(),
it()..isA<MessageListRecipientHeaderItem>(),
it()..isA<MessageListMessageItem>().showSender.isTrue(),
it()..isA<MessageListMarkReadItem>(),
]);
});

Expand Down Expand Up @@ -822,6 +823,9 @@ void checkInvariants(MessageListView model) {
..isLastInBlock.equals(
i == model.items.length || model.items[i] is! MessageListMessageItem);
}
if (model.items.length == i + 1) {
check(model.items[i++]).isA<MessageListMarkReadItem>();
}
check(model.items).length.equals(i);
}

Expand Down
7 changes: 5 additions & 2 deletions test/widgets/message_list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import 'package:checks/checks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:zulip/api/model/events.dart';
import 'package:zulip/api/model/model.dart';
Expand Down Expand Up @@ -49,6 +50,8 @@ void main() {

await tester.pumpWidget(
MaterialApp(
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
supportedLocales: ZulipLocalizations.supportedLocales,
home: GlobalStoreWidget(
child: PerAccountStoreWidget(
accountId: eg.selfAccount.id,
Expand All @@ -70,7 +73,7 @@ void main() {
testWidgets('basic', (tester) async {
await setupMessageListPage(tester, foundOldest: false,
messages: List.generate(200, (i) => eg.streamMessage(id: 950 + i, sender: eg.selfUser)));
check(itemCount(tester)).equals(201);
check(itemCount(tester)).equals(202);

// Fling-scroll upward...
await tester.fling(find.byType(MessageListPage), const Offset(0, 300), 8000);
Expand All @@ -83,7 +86,7 @@ void main() {
await tester.pump(Duration.zero); // Allow a frame for the response to arrive.

// Now we have more messages.
check(itemCount(tester)).equals(301);
check(itemCount(tester)).equals(302);
});

testWidgets('observe double-fetch glitch', (tester) async {
Expand Down

0 comments on commit 1cdb83a

Please sign in to comment.