Skip to content

Commit f88f532

Browse files
committed
Merge remote-tracking branch 'pr/1561'
2 parents a54bd67 + 9c82301 commit f88f532

16 files changed

+451
-93
lines changed

lib/model/autocomplete.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'compose.dart';
1212
import 'emoji.dart';
1313
import 'narrow.dart';
1414
import 'store.dart';
15+
import 'user.dart';
1516

1617
extension ComposeContentAutocomplete on ComposeContentController {
1718
AutocompleteIntent<ComposeAutocompleteQuery>? autocompleteIntent() {
@@ -648,7 +649,7 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
648649
}
649650

650651
MentionAutocompleteResult? _testUser(MentionAutocompleteQuery query, User user) {
651-
if (query.testUser(user, store.autocompleteViewManager.autocompleteDataCache)) {
652+
if (query.testUser(user, store.autocompleteViewManager.autocompleteDataCache, store)) {
652653
return UserMentionAutocompleteResult(userId: user.userId);
653654
}
654655
return null;
@@ -753,9 +754,10 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
753754
|| wildcardOption.localizedCanonicalString(localizations).contains(_lowercase);
754755
}
755756

756-
bool testUser(User user, AutocompleteDataCache cache) {
757+
bool testUser(User user, AutocompleteDataCache cache, UserStore store) {
757758
// TODO(#236) test email too, not just name
758759
if (!user.isActive) return false;
760+
if (store.isUserMuted(user.userId)) return false;
759761

760762
return _testName(user, cache);
761763
}

lib/model/channel.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ mixin ChannelStore {
6969

7070
/// Whether the given event will change the result of [isTopicVisibleInStream]
7171
/// for its stream and topic, compared to the current state.
72-
VisibilityEffect willChangeIfTopicVisibleInStream(UserTopicEvent event) {
72+
UserTopicVisibilityEffect willChangeIfTopicVisibleInStream(UserTopicEvent event) {
7373
final streamId = event.streamId;
7474
final topic = event.topicName;
75-
return VisibilityEffect._fromBeforeAfter(
75+
return UserTopicVisibilityEffect._fromBeforeAfter(
7676
_isTopicVisibleInStream(topicVisibilityPolicy(streamId, topic)),
7777
_isTopicVisibleInStream(event.visibilityPolicy));
7878
}
@@ -106,10 +106,10 @@ mixin ChannelStore {
106106

107107
/// Whether the given event will change the result of [isTopicVisible]
108108
/// for its stream and topic, compared to the current state.
109-
VisibilityEffect willChangeIfTopicVisible(UserTopicEvent event) {
109+
UserTopicVisibilityEffect willChangeIfTopicVisible(UserTopicEvent event) {
110110
final streamId = event.streamId;
111111
final topic = event.topicName;
112-
return VisibilityEffect._fromBeforeAfter(
112+
return UserTopicVisibilityEffect._fromBeforeAfter(
113113
_isTopicVisible(streamId, topicVisibilityPolicy(streamId, topic)),
114114
_isTopicVisible(streamId, event.visibilityPolicy));
115115
}
@@ -137,7 +137,7 @@ mixin ChannelStore {
137137
/// Whether and how a given [UserTopicEvent] will affect the results
138138
/// that [ChannelStore.isTopicVisible] or [ChannelStore.isTopicVisibleInStream]
139139
/// would give for some messages.
140-
enum VisibilityEffect {
140+
enum UserTopicVisibilityEffect {
141141
/// The event will have no effect on the visibility results.
142142
none,
143143

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

150-
factory VisibilityEffect._fromBeforeAfter(bool before, bool after) {
150+
factory UserTopicVisibilityEffect._fromBeforeAfter(bool before, bool after) {
151151
return switch ((before, after)) {
152-
(false, true) => VisibilityEffect.unmuted,
153-
(true, false) => VisibilityEffect.muted,
154-
_ => VisibilityEffect.none,
152+
(false, true) => UserTopicVisibilityEffect.unmuted,
153+
(true, false) => UserTopicVisibilityEffect.muted,
154+
_ => UserTopicVisibilityEffect.none,
155155
};
156156
}
157157
}

lib/model/message.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,12 @@ class MessageStoreImpl extends PerAccountStoreBase with MessageStore, _OutboxMes
287287
}
288288
}
289289

290+
void handleMutedUsersEvent(MutedUsersEvent event) {
291+
for (final view in _messageListViews) {
292+
view.handleMutedUsersEvent(event);
293+
}
294+
}
295+
290296
void handleMessageEvent(MessageEvent event) {
291297
// If the message is one we already know about (from a fetch),
292298
// clobber it with the one from the event system.

lib/model/message_list.dart

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'content.dart';
1313
import 'message.dart';
1414
import 'narrow.dart';
1515
import 'store.dart';
16+
import 'user.dart';
1617

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

@@ -626,7 +627,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
626627
return switch (message.conversation) {
627628
StreamConversation(:final streamId, :final topic) =>
628629
store.isTopicVisible(streamId, topic),
629-
DmConversation() => true,
630+
DmConversation(:final allRecipientIds) =>
631+
!store.shouldMuteDmConversation(DmNarrow(
632+
allRecipientIds: allRecipientIds, selfUserId: store.selfUserId)),
630633
};
631634

632635
case ChannelNarrow(:final streamId):
@@ -637,45 +640,67 @@ class MessageListView with ChangeNotifier, _MessageSequence {
637640

638641
case TopicNarrow():
639642
case DmNarrow():
643+
return true;
644+
645+
case MentionsNarrow():
646+
case StarredMessagesNarrow():
647+
if (message.conversation case DmConversation(:final allRecipientIds)) {
648+
return !store.shouldMuteDmConversation(DmNarrow(
649+
allRecipientIds: allRecipientIds, selfUserId: store.selfUserId));
650+
}
651+
return true;
652+
}
653+
}
654+
655+
/// Whether [_messageVisible] is true for all possible messages.
656+
///
657+
/// This is useful for an optimization.
658+
bool get _allMessagesVisible {
659+
switch (narrow) {
660+
case CombinedFeedNarrow():
661+
case ChannelNarrow():
640662
case MentionsNarrow():
641663
case StarredMessagesNarrow():
664+
return false;
665+
666+
case TopicNarrow():
667+
case DmNarrow():
642668
return true;
643669
}
644670
}
645671

646672
/// Whether this event could affect the result that [_messageVisible]
647673
/// would ever have returned for any possible message in this message list.
648-
VisibilityEffect _canAffectVisibility(UserTopicEvent event) {
674+
UserTopicVisibilityEffect _userTopicEventCanAffectVisibility(UserTopicEvent event) {
649675
switch (narrow) {
650676
case CombinedFeedNarrow():
651677
return store.willChangeIfTopicVisible(event);
652678

653679
case ChannelNarrow(:final streamId):
654-
if (event.streamId != streamId) return VisibilityEffect.none;
680+
if (event.streamId != streamId) return UserTopicVisibilityEffect.none;
655681
return store.willChangeIfTopicVisibleInStream(event);
656682

657683
case TopicNarrow():
658684
case DmNarrow():
659685
case MentionsNarrow():
660686
case StarredMessagesNarrow():
661-
return VisibilityEffect.none;
687+
return UserTopicVisibilityEffect.none;
662688
}
663689
}
664690

665-
/// Whether [_messageVisible] is true for all possible messages.
666-
///
667-
/// This is useful for an optimization.
668-
bool get _allMessagesVisible {
669-
switch (narrow) {
691+
/// Whether this event could affect the result that [_messageVisible]
692+
/// would ever have returned for any possible message in this message list.
693+
MutedUsersVisibilityEffect _mutedUsersEventCanAffectVisibility(MutedUsersEvent event) {
694+
switch(narrow) {
670695
case CombinedFeedNarrow():
671-
case ChannelNarrow():
672-
return false;
696+
case MentionsNarrow():
697+
case StarredMessagesNarrow():
698+
return store.mightChangeShouldMuteDmConversation(event);
673699

700+
case ChannelNarrow():
674701
case TopicNarrow():
675702
case DmNarrow():
676-
case MentionsNarrow():
677-
case StarredMessagesNarrow():
678-
return true;
703+
return MutedUsersVisibilityEffect.none;
679704
}
680705
}
681706

@@ -944,26 +969,26 @@ class MessageListView with ChangeNotifier, _MessageSequence {
944969
}
945970

946971
void handleUserTopicEvent(UserTopicEvent event) {
947-
switch (_canAffectVisibility(event)) {
948-
case VisibilityEffect.none:
972+
switch (_userTopicEventCanAffectVisibility(event)) {
973+
case UserTopicVisibilityEffect.none:
949974
return;
950975

951-
case VisibilityEffect.muted:
976+
case UserTopicVisibilityEffect.muted:
952977
bool removed = _removeOutboxMessagesWhere((message) =>
953978
message is StreamOutboxMessage
954979
&& message.conversation.streamId == event.streamId
955980
&& message.conversation.topic == event.topicName);
956981

957982
removed |= _removeMessagesWhere((message) =>
958983
message is StreamMessage
959-
&& message.streamId == event.streamId
984+
&& message.streamId == event.streamId
960985
&& message.topic == event.topicName);
961986

962987
if (removed) {
963988
notifyListeners();
964989
}
965990

966-
case VisibilityEffect.unmuted:
991+
case UserTopicVisibilityEffect.unmuted:
967992
// TODO get the newly-unmuted messages from the message store
968993
// For now, we simplify the task by just refetching this message list
969994
// from scratch.
@@ -987,6 +1012,36 @@ class MessageListView with ChangeNotifier, _MessageSequence {
9871012
}
9881013
}
9891014

1015+
void handleMutedUsersEvent(MutedUsersEvent event) {
1016+
final newMutedUsers = {...event.mutedUsers.map((e) => e.id)};
1017+
1018+
switch (_mutedUsersEventCanAffectVisibility(event)) {
1019+
case MutedUsersVisibilityEffect.none:
1020+
return;
1021+
1022+
case MutedUsersVisibilityEffect.muted:
1023+
final anyRemoved = _removeMessagesWhere((message) {
1024+
if (message is! DmMessage) return false;
1025+
final narrow = DmNarrow.ofMessage(message, selfUserId: store.selfUserId);
1026+
return store.shouldMuteDmConversation(narrow, mutedUsers: newMutedUsers);
1027+
});
1028+
if (anyRemoved) {
1029+
notifyListeners();
1030+
}
1031+
1032+
case MutedUsersVisibilityEffect.mixed:
1033+
case MutedUsersVisibilityEffect.unmuted:
1034+
// TODO get the newly-unmuted messages from the message store
1035+
// For now, we simplify the task by just refetching this message list
1036+
// from scratch.
1037+
if (fetched) {
1038+
_reset();
1039+
notifyListeners();
1040+
fetchInitial();
1041+
}
1042+
}
1043+
}
1044+
9901045
void handleDeleteMessageEvent(DeleteMessageEvent event) {
9911046
if (_removeMessagesById(event.messageIds)) {
9921047
notifyListeners();

lib/model/store.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,10 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
649649
bool isUserMuted(int userId, {Set<int>? mutedUsers}) =>
650650
_users.isUserMuted(userId, mutedUsers: mutedUsers);
651651

652+
@override
653+
MutedUsersVisibilityEffect mightChangeShouldMuteDmConversation(MutedUsersEvent event) =>
654+
_users.mightChangeShouldMuteDmConversation(event);
655+
652656
final UserStoreImpl _users;
653657

654658
final TypingStatus typingStatus;
@@ -958,6 +962,8 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
958962

959963
case MutedUsersEvent():
960964
assert(debugLog("server event: muted_users"));
965+
_messages.handleMutedUsersEvent(event);
966+
// Update _users last, so other handlers can compare to the old value.
961967
_users.handleMutedUsersEvent(event);
962968
notifyListeners();
963969

lib/model/typing_status.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ class TypingStatus extends PerAccountStoreBase with ChangeNotifier {
2121

2222
Iterable<SendableNarrow> get debugActiveNarrows => _timerMapsByNarrow.keys;
2323

24-
Iterable<int> typistIdsInNarrow(SendableNarrow narrow) =>
25-
_timerMapsByNarrow[narrow]?.keys ?? [];
24+
Iterable<int>? typistIdsInNarrow(SendableNarrow narrow) =>
25+
_timerMapsByNarrow[narrow]?.keys;
2626

2727
// Using SendableNarrow as the key covers the narrows
2828
// where typing notices are supported (topics and DMs).

lib/model/user.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import '../api/model/events.dart';
22
import '../api/model/initial_snapshot.dart';
33
import '../api/model/model.dart';
4+
import 'algorithms.dart';
45
import 'localizations.dart';
6+
import 'narrow.dart';
57
import 'store.dart';
68

79
/// The portion of [PerAccountStore] describing the users in the realm.
@@ -85,6 +87,38 @@ mixin UserStore on PerAccountStoreBase {
8587
/// Looks for [userId] in a private [Set],
8688
/// or in [mutedUsers] instead if that's non-null.
8789
bool isUserMuted(int userId, {Set<int>? mutedUsers});
90+
91+
/// Whether the self-user has muted everyone in [narrow].
92+
///
93+
/// Returns false for the self-DM.
94+
///
95+
/// Calls [isUserMuted] for each participant, passing along [mutedUsers].
96+
bool shouldMuteDmConversation(DmNarrow narrow, {Set<int>? mutedUsers}) {
97+
if (narrow.otherRecipientIds.isEmpty) return false;
98+
return narrow.otherRecipientIds.every(
99+
(userId) => isUserMuted(userId, mutedUsers: mutedUsers));
100+
}
101+
102+
/// Whether the given event might change the result of [shouldMuteDmConversation]
103+
/// for its list of muted users, compared to the current state.
104+
MutedUsersVisibilityEffect mightChangeShouldMuteDmConversation(MutedUsersEvent event);
105+
}
106+
107+
/// Whether and how a given [MutedUsersEvent] may affect the results
108+
/// that [UserStore.shouldMuteDmConversation] would give for some messages.
109+
enum MutedUsersVisibilityEffect {
110+
/// The event will have no effect on the visibility results.
111+
none,
112+
113+
/// The event may change some visibility results from true to false.
114+
muted,
115+
116+
/// The event may change some visibility results from false to true.
117+
unmuted,
118+
119+
/// The event may change some visibility results from false to true,
120+
/// and some from true to false.
121+
mixed;
88122
}
89123

90124
/// The implementation of [UserStore] that does the work.
@@ -118,6 +152,29 @@ class UserStoreImpl extends PerAccountStoreBase with UserStore {
118152
return (mutedUsers ?? _mutedUsers).contains(userId);
119153
}
120154

155+
@override
156+
MutedUsersVisibilityEffect mightChangeShouldMuteDmConversation(MutedUsersEvent event) {
157+
final sortedOld = _mutedUsers.toList()..sort();
158+
final sortedNew = event.mutedUsers.map((u) => u.id).toList()..sort();
159+
assert(isSortedWithoutDuplicates(sortedOld));
160+
assert(isSortedWithoutDuplicates(sortedNew));
161+
final union = setUnion(sortedOld, sortedNew);
162+
163+
final willMuteSome = sortedOld.length < union.length;
164+
final willUnmuteSome = sortedNew.length < union.length;
165+
166+
switch ((willUnmuteSome, willMuteSome)) {
167+
case (true, false):
168+
return MutedUsersVisibilityEffect.unmuted;
169+
case (false, true):
170+
return MutedUsersVisibilityEffect.muted;
171+
case (true, true):
172+
return MutedUsersVisibilityEffect.mixed;
173+
case (false, false): // TODO(log)?
174+
return MutedUsersVisibilityEffect.none;
175+
}
176+
}
177+
121178
void handleRealmUserEvent(RealmUserEvent event) {
122179
switch (event) {
123180
case RealmUserAddEvent():

lib/widgets/message_list.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -990,13 +990,16 @@ class _TypingStatusWidgetState extends State<TypingStatusWidget> with PerAccount
990990
final store = PerAccountStoreWidget.of(context);
991991
final zulipLocalizations = ZulipLocalizations.of(context);
992992
final typistIds = model!.typistIdsInNarrow(narrow);
993-
if (typistIds.isEmpty) return const SizedBox();
994-
final text = switch (typistIds.length) {
993+
if (typistIds == null) return const SizedBox();
994+
final filteredTypistIds = typistIds
995+
.whereNot((userId) => store.isUserMuted(userId));
996+
if (filteredTypistIds.isEmpty) return const SizedBox();
997+
final text = switch (filteredTypistIds.length) {
995998
1 => zulipLocalizations.onePersonTyping(
996-
store.userDisplayName(typistIds.first)),
999+
store.userDisplayName(filteredTypistIds.first)),
9971000
2 => zulipLocalizations.twoPeopleTyping(
998-
store.userDisplayName(typistIds.first),
999-
store.userDisplayName(typistIds.last)),
1001+
store.userDisplayName(filteredTypistIds.first),
1002+
store.userDisplayName(filteredTypistIds.last)),
10001003
_ => zulipLocalizations.manyPeopleTyping,
10011004
};
10021005

0 commit comments

Comments
 (0)