Skip to content
8 changes: 5 additions & 3 deletions lib/model/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'compose.dart';
import 'emoji.dart';
import 'narrow.dart';
import 'store.dart';
import 'user.dart';

extension ComposeContentAutocomplete on ComposeContentController {
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent() {
Expand Down Expand Up @@ -649,7 +650,7 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
}

MentionAutocompleteResult? _testUser(MentionAutocompleteQuery query, User user) {
if (query.testUser(user, store.autocompleteViewManager.autocompleteDataCache)) {
if (query.testUser(user, store.autocompleteViewManager.autocompleteDataCache, store)) {
return UserMentionAutocompleteResult(userId: user.userId);
}
return null;
Expand Down Expand Up @@ -754,10 +755,11 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
|| wildcardOption.localizedCanonicalString(localizations).contains(_lowercase);
}

bool testUser(User user, AutocompleteDataCache cache) {
// TODO(#236) test email too, not just name
bool testUser(User user, AutocompleteDataCache cache, UserStore store) {
if (!user.isActive) return false;
if (store.isUserMuted(user.userId)) return false;

// TODO(#236) test email too, not just name
return _testName(user, cache);
}

Expand Down
18 changes: 9 additions & 9 deletions lib/model/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ mixin ChannelStore {

/// Whether the given event will change the result of [isTopicVisibleInStream]
/// for its stream and topic, compared to the current state.
VisibilityEffect willChangeIfTopicVisibleInStream(UserTopicEvent event) {
UserTopicVisibilityEffect willChangeIfTopicVisibleInStream(UserTopicEvent event) {
final streamId = event.streamId;
final topic = event.topicName;
return VisibilityEffect._fromBeforeAfter(
return UserTopicVisibilityEffect._fromBeforeAfter(
_isTopicVisibleInStream(topicVisibilityPolicy(streamId, topic)),
_isTopicVisibleInStream(event.visibilityPolicy));
}
Expand Down Expand Up @@ -106,10 +106,10 @@ mixin ChannelStore {

/// Whether the given event will change the result of [isTopicVisible]
/// for its stream and topic, compared to the current state.
VisibilityEffect willChangeIfTopicVisible(UserTopicEvent event) {
UserTopicVisibilityEffect willChangeIfTopicVisible(UserTopicEvent event) {
final streamId = event.streamId;
final topic = event.topicName;
return VisibilityEffect._fromBeforeAfter(
return UserTopicVisibilityEffect._fromBeforeAfter(
_isTopicVisible(streamId, topicVisibilityPolicy(streamId, topic)),
_isTopicVisible(streamId, event.visibilityPolicy));
}
Expand Down Expand Up @@ -137,7 +137,7 @@ mixin ChannelStore {
/// Whether and how a given [UserTopicEvent] will affect the results
/// that [ChannelStore.isTopicVisible] or [ChannelStore.isTopicVisibleInStream]
/// would give for some messages.
enum VisibilityEffect {
enum UserTopicVisibilityEffect {
/// The event will have no effect on the visibility results.
none,

Expand All @@ -147,11 +147,11 @@ enum VisibilityEffect {
/// The event will change some visibility results from false to true.
unmuted;

factory VisibilityEffect._fromBeforeAfter(bool before, bool after) {
factory UserTopicVisibilityEffect._fromBeforeAfter(bool before, bool after) {
return switch ((before, after)) {
(false, true) => VisibilityEffect.unmuted,
(true, false) => VisibilityEffect.muted,
_ => VisibilityEffect.none,
(false, true) => UserTopicVisibilityEffect.unmuted,
(true, false) => UserTopicVisibilityEffect.muted,
_ => UserTopicVisibilityEffect.none,
Comment on lines -152 to +154
Copy link
Member

Choose a reason for hiding this comment

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

This is awkwardly verbose, but we're about to add another kind of
visibility effect, and I think the code will end up clearer if we
make a separate enum for it.

Yeah, agreed.

Some of the verbosity (like in these lines) will at least get resolved by the upcoming Dart feature of "dot shorthands", expected later this year:
https://github.com/dart-lang/language/blob/main/working/3616%20-%20enum%20value%20shorthand/proposal-simple-lrhn.md

I believe with that feature we'll be able to say just .unmuted, .muted, .none here, without the name of the enum type.

};
}
}
Expand Down
6 changes: 6 additions & 0 deletions lib/model/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,12 @@ class MessageStoreImpl extends PerAccountStoreBase with MessageStore, _OutboxMes
}
}

void handleMutedUsersEvent(MutedUsersEvent event) {
for (final view in _messageListViews) {
view.handleMutedUsersEvent(event);
}
}

void handleMessageEvent(MessageEvent event) {
// If the message is one we already know about (from a fetch),
// clobber it with the one from the event system.
Expand Down
90 changes: 74 additions & 16 deletions lib/model/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'content.dart';
import 'message.dart';
import 'narrow.dart';
import 'store.dart';
import 'user.dart';

export '../api/route/messages.dart' show Anchor, AnchorCode, NumericAnchor;

Expand Down Expand Up @@ -668,10 +669,12 @@ class MessageListView with ChangeNotifier, _MessageSequence {
bool _messageVisible(MessageBase message) {
switch (narrow) {
case CombinedFeedNarrow():
return switch (message.conversation) {
final conversation = message.conversation;
return switch (conversation) {
StreamConversation(:final streamId, :final topic) =>
store.isTopicVisible(streamId, topic),
DmConversation() => true,
DmConversation() => !store.shouldMuteDmConversation(
DmNarrow.ofConversation(conversation, selfUserId: store.selfUserId)),
};

case ChannelNarrow(:final streamId):
Expand All @@ -682,48 +685,75 @@ class MessageListView with ChangeNotifier, _MessageSequence {

case TopicNarrow():
case DmNarrow():
return true;

case MentionsNarrow():
case StarredMessagesNarrow():
case KeywordSearchNarrow():
if (message.conversation case DmConversation(:final allRecipientIds)) {
return !store.shouldMuteDmConversation(DmNarrow(
allRecipientIds: allRecipientIds, selfUserId: store.selfUserId));
}
return true;
}
}

/// Whether [_messageVisible] is true for all possible messages.
///
/// This is useful for an optimization.
bool get _allMessagesVisible {
switch (narrow) {
case CombinedFeedNarrow():
case ChannelNarrow():
return false;

case TopicNarrow():
case DmNarrow():
return true;

case MentionsNarrow():
case StarredMessagesNarrow():
case KeywordSearchNarrow():
return false;
}
}

/// Whether this event could affect the result that [_messageVisible]
/// would ever have returned for any possible message in this message list.
VisibilityEffect _canAffectVisibility(UserTopicEvent event) {
UserTopicVisibilityEffect _canAffectVisibility(UserTopicEvent event) {
switch (narrow) {
case CombinedFeedNarrow():
return store.willChangeIfTopicVisible(event);

case ChannelNarrow(:final streamId):
if (event.streamId != streamId) return VisibilityEffect.none;
if (event.streamId != streamId) return UserTopicVisibilityEffect.none;
return store.willChangeIfTopicVisibleInStream(event);

case TopicNarrow():
case DmNarrow():
case MentionsNarrow():
case StarredMessagesNarrow():
case KeywordSearchNarrow():
return VisibilityEffect.none;
return UserTopicVisibilityEffect.none;
}
}

/// Whether [_messageVisible] is true for all possible messages.
///
/// This is useful for an optimization.
bool get _allMessagesVisible {
switch (narrow) {
/// Whether this event could affect the result that [_messageVisible]
/// would ever have returned for any possible message in this message list.
MutedUsersVisibilityEffect _mutedUsersEventCanAffectVisibility(MutedUsersEvent event) {
switch(narrow) {
case CombinedFeedNarrow():
Comment on lines +743 to 745
Copy link
Member

Choose a reason for hiding this comment

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

similarly, put the cases in the same order as in _messageVisible

case ChannelNarrow():
return false;
return store.mightChangeShouldMuteDmConversation(event);

case ChannelNarrow():
case TopicNarrow():
case DmNarrow():
return MutedUsersVisibilityEffect.none;

case MentionsNarrow():
case StarredMessagesNarrow():
case KeywordSearchNarrow():
return true;
return store.mightChangeShouldMuteDmConversation(event);
}
}

Expand Down Expand Up @@ -1011,10 +1041,10 @@ class MessageListView with ChangeNotifier, _MessageSequence {

void handleUserTopicEvent(UserTopicEvent event) {
switch (_canAffectVisibility(event)) {
case VisibilityEffect.none:
case UserTopicVisibilityEffect.none:
return;

case VisibilityEffect.muted:
case UserTopicVisibilityEffect.muted:
bool removed = _removeMessagesWhere((message) =>
message is StreamMessage
&& message.streamId == event.streamId
Expand All @@ -1029,7 +1059,35 @@ class MessageListView with ChangeNotifier, _MessageSequence {
notifyListeners();
}

case VisibilityEffect.unmuted:
case UserTopicVisibilityEffect.unmuted:
// TODO get the newly-unmuted messages from the message store
// For now, we simplify the task by just refetching this message list
// from scratch.
if (fetched) {
_reset();
notifyListeners();
fetchInitial();
}
}
}

void handleMutedUsersEvent(MutedUsersEvent event) {
switch (_mutedUsersEventCanAffectVisibility(event)) {
case MutedUsersVisibilityEffect.none:
return;

case MutedUsersVisibilityEffect.muted:
final anyRemoved = _removeMessagesWhere((message) {
if (message is! DmMessage) return false;
final narrow = DmNarrow.ofMessage(message, selfUserId: store.selfUserId);
return store.shouldMuteDmConversation(narrow, event: event);
});
if (anyRemoved) {
notifyListeners();
}

case MutedUsersVisibilityEffect.mixed:
case MutedUsersVisibilityEffect.unmuted:
// TODO get the newly-unmuted messages from the message store
// For now, we simplify the task by just refetching this message list
// from scratch.
Expand Down
10 changes: 10 additions & 0 deletions lib/model/narrow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,21 @@ class DmNarrow extends Narrow implements SendableNarrow {
required int selfUserId,
}) {
return DmNarrow(
// TODO should this really be making a copy of `allRecipientIds`?
allRecipientIds: List.unmodifiable(message.conversation.allRecipientIds),
selfUserId: selfUserId,
);
}

factory DmNarrow.ofConversation(DmConversation conversation, {
required int selfUserId,
}) {
return DmNarrow(
allRecipientIds: conversation.allRecipientIds,
selfUserId: selfUserId,
);
}

/// A [DmNarrow] from an item in [InitialSnapshot.recentPrivateConversations].
factory DmNarrow.ofRecentDmConversation(RecentDmConversation conversation, {required int selfUserId}) {
return DmNarrow.withOtherUsers(conversation.userIds, selfUserId: selfUserId);
Expand Down
6 changes: 6 additions & 0 deletions lib/model/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,10 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
bool isUserMuted(int userId, {MutedUsersEvent? event}) =>
_users.isUserMuted(userId, event: event);

@override
MutedUsersVisibilityEffect mightChangeShouldMuteDmConversation(MutedUsersEvent event) =>
_users.mightChangeShouldMuteDmConversation(event);

final UserStoreImpl _users;

final TypingStatus typingStatus;
Expand Down Expand Up @@ -983,6 +987,8 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor

case MutedUsersEvent():
assert(debugLog("server event: muted_users"));
_messages.handleMutedUsersEvent(event);
// Update _users last, so other handlers can compare to the old value.
_users.handleMutedUsersEvent(event);
notifyListeners();

Expand Down
2 changes: 1 addition & 1 deletion lib/model/typing_status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class TypingStatus extends PerAccountStoreBase with ChangeNotifier {
Iterable<SendableNarrow> get debugActiveNarrows => _timerMapsByNarrow.keys;

Iterable<int> typistIdsInNarrow(SendableNarrow narrow) =>
_timerMapsByNarrow[narrow]?.keys ?? [];
_timerMapsByNarrow[narrow]?.keys ?? const [];

// Using SendableNarrow as the key covers the narrows
// where typing notices are supported (topics and DMs).
Expand Down
57 changes: 57 additions & 0 deletions lib/model/user.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import '../api/model/events.dart';
import '../api/model/initial_snapshot.dart';
import '../api/model/model.dart';
import 'algorithms.dart';
import 'localizations.dart';
import 'narrow.dart';
import 'store.dart';

/// The portion of [PerAccountStore] describing the users in the realm.
Expand Down Expand Up @@ -85,6 +87,38 @@ mixin UserStore on PerAccountStoreBase {
/// Looks for [userId] in a private [Set],
/// or in [event.mutedUsers] instead if event is non-null.
bool isUserMuted(int userId, {MutedUsersEvent? event});

/// Whether the self-user has muted everyone in [narrow].
///
/// Returns false for the self-DM.
///
/// Calls [isUserMuted] for each participant, passing along [event].
bool shouldMuteDmConversation(DmNarrow narrow, {MutedUsersEvent? event}) {
if (narrow.otherRecipientIds.isEmpty) return false;
return narrow.otherRecipientIds.every(
(userId) => isUserMuted(userId, event: event));
}

/// Whether the given event might change the result of [shouldMuteDmConversation]
/// for its list of muted users, compared to the current state.
MutedUsersVisibilityEffect mightChangeShouldMuteDmConversation(MutedUsersEvent event);
}

/// Whether and how a given [MutedUsersEvent] may affect the results
/// that [UserStore.shouldMuteDmConversation] would give for some messages.
enum MutedUsersVisibilityEffect {
/// The event will have no effect on the visibility results.
none,

/// The event may change some visibility results from true to false.
muted,

/// The event may change some visibility results from false to true.
unmuted,

/// The event may change some visibility results from false to true,
/// and some from true to false.
mixed;
}

/// The implementation of [UserStore] that does the work.
Expand Down Expand Up @@ -118,6 +152,29 @@ class UserStoreImpl extends PerAccountStoreBase with UserStore {
return (event?.mutedUsers.map((item) => item.id) ?? _mutedUsers).contains(userId);
}

@override
MutedUsersVisibilityEffect mightChangeShouldMuteDmConversation(MutedUsersEvent event) {
final sortedOld = _mutedUsers.toList()..sort();
final sortedNew = event.mutedUsers.map((u) => u.id).toList()..sort();
assert(isSortedWithoutDuplicates(sortedOld));
assert(isSortedWithoutDuplicates(sortedNew));
final union = setUnion(sortedOld, sortedNew);

final willMuteSome = sortedOld.length < union.length;
final willUnmuteSome = sortedNew.length < union.length;
Comment on lines +161 to +164
Copy link
Member

Choose a reason for hiding this comment

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

Neat, yeah, this logic works.


switch ((willUnmuteSome, willMuteSome)) {
case (true, false):
return MutedUsersVisibilityEffect.unmuted;
case (false, true):
return MutedUsersVisibilityEffect.muted;
case (true, true):
return MutedUsersVisibilityEffect.mixed;
case (false, false): // TODO(log)?
return MutedUsersVisibilityEffect.none;
}
}

void handleRealmUserEvent(RealmUserEvent event) {
switch (event) {
case RealmUserAddEvent():
Expand Down
Loading