Skip to content

Commit 4b12d4f

Browse files
authored
feat(llc): Add user-level privacy settings (#2430)
1 parent 166d858 commit 4b12d4f

29 files changed

+1694
-230
lines changed

packages/stream_chat/CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
## Upcoming
2+
3+
✅ Added
4+
5+
- Added support for user-level privacy settings via `OwnUser.privacySettings`.
6+
- Added `invisible` field to `User` and `OwnUser` models.
7+
8+
⚠️ Deprecated
9+
10+
- Deprecated `Channel.canSendTypingEvents` in favor of `Channel.canUseTypingEvents`.
11+
12+
🔄 Changed
13+
14+
- Typing and read receipts now respect both channel capabilities and user privacy settings.
15+
- `markRead`, `markUnread`, `markThreadRead`, and `markThreadUnread` methods now throw
16+
`StreamChatError` when channel lacks required capabilities.
17+
118
## 9.19.0
219

320
- Minor bug fixes and improvements

packages/stream_chat/lib/src/client/channel.dart

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,14 @@ class Channel {
16411641
/// read from a particular message onwards.
16421642
Future<EmptyResponse> markRead({String? messageId}) async {
16431643
_checkInitialized();
1644+
1645+
if (!canReceiveReadEvents) {
1646+
throw const StreamChatError(
1647+
'Cannot mark as read: Channel does not support read events. '
1648+
'Enable read_events in your channel type configuration.',
1649+
);
1650+
}
1651+
16441652
return _client.markChannelRead(id!, type, messageId: messageId);
16451653
}
16461654

@@ -1650,19 +1658,43 @@ class Channel {
16501658
/// to be marked as unread.
16511659
Future<EmptyResponse> markUnread(String messageId) async {
16521660
_checkInitialized();
1661+
1662+
if (!canReceiveReadEvents) {
1663+
throw const StreamChatError(
1664+
'Cannot mark as unread: Channel does not support read events. '
1665+
'Enable read_events in your channel type configuration.',
1666+
);
1667+
}
1668+
16531669
return _client.markChannelUnread(id!, type, messageId);
16541670
}
16551671

16561672
/// Mark the thread with [threadId] in the channel as read.
1657-
Future<EmptyResponse> markThreadRead(String threadId) {
1673+
Future<EmptyResponse> markThreadRead(String threadId) async {
16581674
_checkInitialized();
1659-
return client.markThreadRead(id!, type, threadId);
1675+
1676+
if (!canReceiveReadEvents) {
1677+
throw const StreamChatError(
1678+
'Cannot mark thread as read: Channel does not support read events. '
1679+
'Enable read_events in your channel type configuration.',
1680+
);
1681+
}
1682+
1683+
return _client.markThreadRead(id!, type, threadId);
16601684
}
16611685

16621686
/// Mark the thread with [threadId] in the channel as unread.
1663-
Future<EmptyResponse> markThreadUnread(String threadId) {
1687+
Future<EmptyResponse> markThreadUnread(String threadId) async {
16641688
_checkInitialized();
1665-
return client.markThreadUnread(id!, type, threadId);
1689+
1690+
if (!canReceiveReadEvents) {
1691+
throw const StreamChatError(
1692+
'Cannot mark thread as unread: Channel does not support read events. '
1693+
'Enable read_events in your channel type configuration.',
1694+
);
1695+
}
1696+
1697+
return _client.markThreadUnread(id!, type, threadId);
16661698
}
16671699

16681700
void _initState(ChannelState channelState) {
@@ -2066,20 +2098,29 @@ class Channel {
20662098
onStopTyping: stopTyping,
20672099
);
20682100

2101+
// Whether sending typing events is allowed in the channel and by the user
2102+
// privacy settings.
2103+
bool get _canSendTypingEvents {
2104+
final currentUser = client.state.currentUser;
2105+
final typingIndicatorsEnabled = currentUser?.isTypingIndicatorsEnabled;
2106+
2107+
return canUseTypingEvents && (typingIndicatorsEnabled ?? true);
2108+
}
2109+
20692110
/// Sends the [Event.typingStart] event and schedules a timer to invoke the
20702111
/// [Event.typingStop] event.
20712112
///
20722113
/// This is meant to be called every time the user presses a key.
20732114
Future<void> keyStroke([String? parentId]) async {
2074-
if (config?.typingEvents == false) return;
2115+
if (!_canSendTypingEvents) return;
20752116

20762117
client.logger.info('KeyStroke received');
20772118
return _keyStrokeHandler(parentId);
20782119
}
20792120

20802121
/// Sends the [EventType.typingStart] event.
20812122
Future<void> startTyping([String? parentId]) async {
2082-
if (config?.typingEvents == false) return;
2123+
if (!_canSendTypingEvents) return;
20832124

20842125
client.logger.info('start typing');
20852126
await sendEvent(Event(
@@ -2090,7 +2131,7 @@ class Channel {
20902131

20912132
/// Sends the [EventType.typingStop] event.
20922133
Future<void> stopTyping([String? parentId]) async {
2093-
if (config?.typingEvents == false) return;
2134+
if (!_canSendTypingEvents) return;
20942135

20952136
client.logger.info('stop typing');
20962137
await sendEvent(Event(
@@ -3091,8 +3132,6 @@ class ChannelClientState {
30913132
}
30923133

30933134
void _listenReadEvents() {
3094-
if (_channelState.channel?.config.readEvents == false) return;
3095-
30963135
_subscriptions
30973136
..add(
30983137
_channel
@@ -3489,17 +3528,17 @@ class ChannelClientState {
34893528
final _typingEventsController = BehaviorSubject.seeded(<User, Event>{});
34903529

34913530
void _listenTypingEvents() {
3492-
if (_channelState.channel?.config.typingEvents == false) return;
3493-
3494-
final currentUser = _channel.client.state.currentUser;
3495-
if (currentUser == null) return;
3496-
34973531
_subscriptions
34983532
..add(
34993533
_channel.on(EventType.typingStart).listen(
35003534
(event) {
35013535
final user = event.user;
3502-
if (user != null && user.id != currentUser.id) {
3536+
if (user == null) return;
3537+
3538+
final currentUser = _channel.client.state.currentUser;
3539+
if (currentUser == null) return;
3540+
3541+
if (user.id != currentUser.id) {
35033542
final events = {...typingEvents};
35043543
events[user] = event;
35053544
_typingEventsController.safeAdd(events);
@@ -3511,7 +3550,12 @@ class ChannelClientState {
35113550
_channel.on(EventType.typingStop).listen(
35123551
(event) {
35133552
final user = event.user;
3514-
if (user != null && user.id != currentUser.id) {
3553+
if (user == null) return;
3554+
3555+
final currentUser = _channel.client.state.currentUser;
3556+
if (currentUser == null) return;
3557+
3558+
if (user.id != currentUser.id) {
35153559
final events = {...typingEvents}..remove(user);
35163560
_typingEventsController.safeAdd(events);
35173561
}
@@ -3526,8 +3570,6 @@ class ChannelClientState {
35263570
// the sender due to technical difficulties. e.g. process death, loss of
35273571
// Internet connection or custom implementation.
35283572
void _startCleaningStaleTypingEvents() {
3529-
if (_channelState.channel?.config.typingEvents == false) return;
3530-
35313573
_staleTypingEventsCleanerTimer = Timer.periodic(
35323574
const Duration(seconds: 1),
35333575
(_) {
@@ -3703,7 +3745,9 @@ extension ChannelCapabilityCheck on Channel {
37033745
}
37043746

37053747
/// True, if the current user can send typing events.
3748+
@Deprecated('Use canUseTypingEvents instead')
37063749
bool get canSendTypingEvents {
3750+
if (canUseTypingEvents) return true;
37073751
return ownCapabilities.contains(ChannelCapability.sendTypingEvents);
37083752
}
37093753

packages/stream_chat/lib/src/client/client.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2223,7 +2223,6 @@ class ClientState {
22232223
totalUnreadCount: currentUser?.totalUnreadCount,
22242224
unreadChannels: currentUser?.unreadChannels,
22252225
unreadThreads: currentUser?.unreadThreads,
2226-
blockedUserIds: currentUser?.blockedUserIds,
22272226
pushPreferences: currentUser?.pushPreferences,
22282227
);
22292228
}

packages/stream_chat/lib/src/core/models/channel_model.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ extension type const ChannelCapability(String capability) implements String {
320320
static const searchMessages = ChannelCapability('search-messages');
321321

322322
/// Ability to send typing events.
323+
@Deprecated('Use typingEvents instead')
323324
static const sendTypingEvents = ChannelCapability('send-typing-events');
324325

325326
/// Ability to upload message attachments.

packages/stream_chat/lib/src/core/models/channel_mute.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import 'package:stream_chat/src/core/models/user.dart';
55
part 'channel_mute.g.dart';
66

77
/// The class that contains the information about a muted channel
8-
@JsonSerializable(createToJson: false)
8+
@JsonSerializable()
99
class ChannelMute {
1010
/// Constructor used for json serialization
1111
ChannelMute({
@@ -34,4 +34,7 @@ class ChannelMute {
3434

3535
/// The date in which the mute expires
3636
final DateTime? expires;
37+
38+
/// Serialize to json
39+
Map<String, dynamic> toJson() => _$ChannelMuteToJson(this);
3740
}

packages/stream_chat/lib/src/core/models/channel_mute.g.dart

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/stream_chat/lib/src/core/models/mute.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'package:stream_chat/src/core/models/user.dart';
44
part 'mute.g.dart';
55

66
/// The class that contains the information about a muted user
7-
@JsonSerializable(createToJson: false)
7+
@JsonSerializable()
88
class Mute {
99
/// Constructor used for json serialization
1010
Mute({
@@ -32,4 +32,7 @@ class Mute {
3232

3333
/// The date in which the mute expires
3434
final DateTime? expires;
35+
36+
/// Serialize to json
37+
Map<String, dynamic> toJson() => _$MuteToJson(this);
3538
}

packages/stream_chat/lib/src/core/models/mute.g.dart

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)