Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/api/model/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ class UserSettingsUpdateEvent extends Event {
switch (UserSettingName.fromRawString(json['property'] as String)) {
case UserSettingName.twentyFourHourTime:
return TwentyFourHourTimeMode.fromApiValue(value as bool?);
case UserSettingName.starredMessageCounts:
case UserSettingName.displayEmojiReactionUsers:
return value as bool;
case UserSettingName.emojiset:
Expand Down
1 change: 1 addition & 0 deletions lib/api/model/events.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions lib/api/model/initial_snapshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class InitialSnapshot {

final UnreadMessagesSnapshot unreadMsgs;

final List<int> starredMessages;

final List<ZulipStream> streams;

// In register-queue, the name of this field is the singular "user_status",
Expand Down Expand Up @@ -178,6 +180,7 @@ class InitialSnapshot {
required this.subscriptions,
required this.channelFolders,
required this.unreadMsgs,
required this.starredMessages,
required this.streams,
required this.userStatuses,
required this.userSettings,
Expand Down Expand Up @@ -297,6 +300,7 @@ class UserSettings {
)
TwentyFourHourTimeMode twentyFourHourTime;

bool starredMessageCounts;
bool displayEmojiReactionUsers;
@JsonKey(unknownEnumValue: Emojiset.unknown)
Emojiset emojiset;
Expand All @@ -310,6 +314,7 @@ class UserSettings {

UserSettings({
required this.twentyFourHourTime,
required this.starredMessageCounts,
required this.displayEmojiReactionUsers,
required this.emojiset,
required this.presenceEnabled,
Expand Down
7 changes: 7 additions & 0 deletions lib/api/model/initial_snapshot.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ class UserStatusChange {
@JsonEnum(fieldRename: FieldRename.snake, alwaysCreate: true)
enum UserSettingName {
twentyFourHourTime,
starredMessageCounts,
displayEmojiReactionUsers,
emojiset,
presenceEnabled,
Expand Down
1 change: 1 addition & 0 deletions lib/api/model/model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/api/route/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Future<void> updateSettings(ApiConnection connection, {
// TODO(server-future) allow localeDefault for servers that support it
assert(mode != TwentyFourHourTimeMode.localeDefault);
value = mode.toJson();
case UserSettingName.starredMessageCounts:
case UserSettingName.displayEmojiReactionUsers:
value = valueRaw as bool;
case UserSettingName.emojiset:
Expand Down
56 changes: 50 additions & 6 deletions lib/model/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ mixin MessageStore on ChannelStore {
/// All known messages, indexed by [Message.id].
Map<int, Message> get messages;

/// All starred messages, as message IDs.
Set<int> get starredMessages;
Comment on lines +27 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see.

I'm trying to think if there are other cases where this should get updated. I guess maybe not:

  • We can learn of messages through handleMessageEvent. But maybe those can't ever be starred.
  • We can learn of messages through reconcileMessages. But maybe if those are starred then they should already be in this set.

I think it'd be good to make that reasoning explicit, though, in those methods. Probably including asserts to verify those expectations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think both of those "maybe"s are true, modulo the fetch/event race for reconcileMessages. I'll add some comments, and a TODO(log) for the handleMessageEvent case. (Not for reconcileMessages, though, because of the fetch/event race.)

I think asserts wouldn't be right since it's about our expectations of the server rather than our own code; does that sound right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah — agreed re these being not actually right for asserts.


/// [OutboxMessage]s sent by the user, indexed by [OutboxMessage.localMessageId].
Map<int, OutboxMessage> get outboxMessages;

Expand Down Expand Up @@ -208,6 +211,8 @@ mixin ProxyMessageStore on MessageStore {
@override
Map<int, Message> get messages => messageStore.messages;
@override
Set<int> get starredMessages => messageStore.starredMessages;
@override
Map<int, OutboxMessage> get outboxMessages => messageStore.outboxMessages;
@override
void registerMessageList(MessageListView view) =>
Expand Down Expand Up @@ -261,14 +266,19 @@ class _EditMessageRequestStatus {
}

class MessageStoreImpl extends HasChannelStore with MessageStore, _OutboxMessageStore {
MessageStoreImpl({required super.channels})
: // There are no messages in InitialSnapshot, so we don't have
// a use case for initializing MessageStore with nonempty [messages].
messages = {};
MessageStoreImpl({
required super.channels,
required List<int> initialStarredMessages,
}) :
messages = {},
starredMessages = Set.of(initialStarredMessages);

@override
final Map<int, Message> messages;

@override
final Set<int> starredMessages;

@override
final Set<MessageListView> _messageListViews = {};

Expand Down Expand Up @@ -410,6 +420,17 @@ class MessageStoreImpl extends HasChannelStore with MessageStore, _OutboxMessage
assert(!_disposed);
for (int i = 0; i < messages.length; i++) {
final message = messages[i];

// TODO(#649) update Unreads, if [Unreads.oldUnreadsMissing]
// TODO(#650) update RecentDmConversationsView

// It's tempting to update [starredMessages] based on fetched messages,
// like Unreads and RecentDmConversationsView.
// But we don't need to: per the API doc, InitialSnapshot.starredMessages
// is the complete list of starred message IDs. So we can maintain it
// using just the event system, avoiding the fetch/event race
// as a cause of inaccuracies.

messages[i] = this.messages.update(message.id,
ifAbsent: () => _reconcileUnrecognizedMessage(message),
(current) => _reconcileRecognizedMessage(current, message));
Expand Down Expand Up @@ -593,6 +614,13 @@ class MessageStoreImpl extends HasChannelStore with MessageStore, _OutboxMessage
void handleMessageEvent(MessageEvent event) {
final message = event.message;

if (message.flags.contains(MessageFlag.starred)) { // TODO(log)
// It would be surprising if a newly-sent message could be starred.
// (I notice that the send-message endpoint doesn't offer a way to
// set the starred flag.)
// If it turns out to be possible, we should update [starredMessages].
}

// If the message is one we already know about (from a fetch),
// clobber it with the one from the event system.
// See [reconcileMessages] for reasoning.
Expand Down Expand Up @@ -716,18 +744,25 @@ class MessageStoreImpl extends HasChannelStore with MessageStore, _OutboxMessage
}
}

void handleDeleteMessageEvent(DeleteMessageEvent event) {
/// Handle a [DeleteMessageEvent]
/// and return true if the [PerAccountStore] should notify listeners.
bool handleDeleteMessageEvent(DeleteMessageEvent event) {
bool perAccountStoreShouldNotify = false;
for (final messageId in event.messageIds) {
messages.remove(messageId);
perAccountStoreShouldNotify |= starredMessages.remove(messageId);
_maybeStaleChannelMessages.remove(messageId);
_editMessageRequests.remove(messageId);
}
for (final view in _messageListViews) {
view.handleDeleteMessageEvent(event);
}
return perAccountStoreShouldNotify;
}

void handleUpdateMessageFlagsEvent(UpdateMessageFlagsEvent event) {
/// Handle an [UpdateMessageFlagsEvent]
/// and return true if the [PerAccountStore] should notify listeners.
bool handleUpdateMessageFlagsEvent(UpdateMessageFlagsEvent event) {
final isAdd = switch (event) {
UpdateMessageFlagsAddEvent() => true,
UpdateMessageFlagsRemoveEvent() => false,
Expand Down Expand Up @@ -765,6 +800,15 @@ class MessageStoreImpl extends HasChannelStore with MessageStore, _OutboxMessage
_notifyMessageListViews(event.messages);
}
}

if (event.flag == MessageFlag.starred) {
isAdd
? starredMessages.addAll(event.messages)
: starredMessages.removeAll(event.messages);
return true;
}

return false;
}

void handleReactionEvent(ReactionEvent event) {
Expand Down
25 changes: 24 additions & 1 deletion lib/model/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,10 @@ class MessageListView with ChangeNotifier, _MessageSequence {
/// This depends in particular on whether the message is muted in
/// one way or another.
///
/// See also [_allMessagesVisible].
/// See also:
/// - [_allMessagesVisible], message-fetch optimization related to this
/// - Message-list UI that mitigates spam/harrassment
/// by obscuring messages from muted senders, with a "reveal" button
bool _messageVisible(MessageBase message) {
switch (narrow) {
case CombinedFeedNarrow():
Expand All @@ -694,7 +697,23 @@ class MessageListView with ChangeNotifier, _MessageSequence {
// If changing this, consider whether [Unreads.countInMentionsNarrow]
// should be changed correspondingly, so the message-list view matches
// the unread-count badge.
if (message.conversation case DmConversation(:final allRecipientIds)) {
return !store.shouldMuteDmConversation(DmNarrow(
allRecipientIds: allRecipientIds, selfUserId: store.selfUserId));
}
return true;

case StarredMessagesNarrow():
// Include messages even if muted in some way.
// Other users can't spam this view by starring messages;
// the starred state is read/write by the self-user only.
//
// If we want to change this, consider that we need to compute a
// starred-message count without relying on fetched message data:
// https://zulip.com/help/star-a-message#view-your-starred-messages
// ([MessageStore.starredMessages] is just a list of message IDs.)
return true;

case KeywordSearchNarrow():
if (message.conversation case DmConversation(:final allRecipientIds)) {
return !store.shouldMuteDmConversation(DmNarrow(
Expand All @@ -718,7 +737,11 @@ class MessageListView with ChangeNotifier, _MessageSequence {
return true;

case MentionsNarrow():
return false;

case StarredMessagesNarrow():
return true;

case KeywordSearchNarrow():
return false;
}
Expand Down
13 changes: 10 additions & 3 deletions lib/model/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,8 @@ class PerAccountStore extends PerAccountStoreBase with
presence: Presence(realm: realm,
initial: initialSnapshot.presences),
channels: channels,
messages: MessageStoreImpl(channels: channels),
messages: MessageStoreImpl(channels: channels,
initialStarredMessages: initialSnapshot.starredMessages),
unreads: Unreads(core: core, channelStore: channels,
initial: initialSnapshot.unreadMsgs),
recentDmConversationsView: RecentDmConversationsView(core: core,
Expand Down Expand Up @@ -783,6 +784,8 @@ class PerAccountStore extends PerAccountStoreBase with
switch (event.property!) {
case UserSettingName.twentyFourHourTime:
userSettings.twentyFourHourTime = event.value as TwentyFourHourTimeMode;
case UserSettingName.starredMessageCounts:
userSettings.starredMessageCounts = event.value as bool;
case UserSettingName.displayEmojiReactionUsers:
userSettings.displayEmojiReactionUsers = event.value as bool;
case UserSettingName.emojiset:
Expand Down Expand Up @@ -889,18 +892,22 @@ class PerAccountStore extends PerAccountStoreBase with

case DeleteMessageEvent():
assert(debugLog("server event: delete_message ${event.messageIds}"));
bool shouldNotify = false;
// This should be called before [_messages.handleDeleteMessageEvent(event)],
// as we need to know about each message for [event.messageIds],
// specifically, their `senderId`s. By calling this after the
// aforementioned line, we'll lose reference to those messages.
recentSenders.handleDeleteMessageEvent(event, messages);
_messages.handleDeleteMessageEvent(event);
shouldNotify |= _messages.handleDeleteMessageEvent(event);
unreads.handleDeleteMessageEvent(event);
if (shouldNotify) notifyListeners();

case UpdateMessageFlagsEvent():
assert(debugLog("server event: update_message_flags/${event.op} ${event.flag.toJson()}"));
_messages.handleUpdateMessageFlagsEvent(event);
bool shouldNotify = false;
shouldNotify |= _messages.handleUpdateMessageFlagsEvent(event);
unreads.handleUpdateMessageFlagsEvent(event);
if (shouldNotify) notifyListeners();

case SubmessageEvent():
assert(debugLog("server event: submessage ${event.content}"));
Expand Down
Loading