Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into feature/allow-custo…
Browse files Browse the repository at this point in the history
…m-member-data

# Conflicts:
#	packages/stream_chat_persistence/CHANGELOG.md
  • Loading branch information
renefloor committed Mar 4, 2025
2 parents d641013 + b4a288a commit 230d083
Show file tree
Hide file tree
Showing 38 changed files with 744 additions and 73 deletions.
9 changes: 9 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@

- [[#1774]](https://github.com/GetStream/stream-chat-flutter/issues/1774) Fixed failed to execute 'close' on 'WebSocket'.
- [[#2016]](https://github.com/GetStream/stream-chat-flutter/issues/2016) Fix muted channel's unreadCount incorrectly updated.

🔄 Changed

- Refactored identifying the `Attachment.uploadState` logic for local and remote attachments. Also updated the logic for determining the attachment type to check for ogScrapeUrl instead of `AttachmentType.giphy`.

✅ Added

- [[#2101]](https://github.com/GetStream/stream-chat-flutter/issues/2101) Added support for system messages not updating `channel.lastMessageAt`
- Added support for sending private or restricted visibility messages.

## 9.4.0

Expand Down
40 changes: 34 additions & 6 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1911,11 +1911,13 @@ class ChannelClientState {
ChannelClientState(
this._channel,
ChannelState channelState,
//ignore: unnecessary_parenthesis
) : _debouncedUpdatePersistenceChannelState = ((ChannelState state) =>
_channel._client.chatPersistenceClient
?.updateChannelState(state))
.debounced(const Duration(seconds: 1)) {
) : _debouncedUpdatePersistenceChannelState = debounce(
(ChannelState state) {
final persistenceClient = _channel._client.chatPersistenceClient;
return persistenceClient?.updateChannelState(state);
},
const Duration(seconds: 1),
) {
_retryQueue = RetryQueue(
channel: _channel,
logger: _channel.client.detachedLogger(
Expand Down Expand Up @@ -2495,6 +2497,25 @@ class ChannelClientState {
}));
}

// Logic taken from the backend SDK
// https://github.com/GetStream/chat/blob/9245c2b3f7e679267d57ee510c60e93de051cb8e/types/channel.go#L1136-L1150
bool _shouldUpdateChannelLastMessageAt(Message message) {
if (message.shadowed) return false;
if (message.isEphemeral) return false;

final config = channelState.channel?.config;
if (message.isSystem && config?.skipLastMsgUpdateForSystemMsgs == true) {
return false;
}

final currentUserId = _channel._client.state.currentUser?.id;
if (currentUserId case final userId? when message.isNotVisibleTo(userId)) {
return false;
}

return true;
}

/// Updates the [message] in the state if it exists. Adds it otherwise.
void updateMessage(Message message) {
// Determine if the message should be displayed in the channel view.
Expand Down Expand Up @@ -2547,12 +2568,19 @@ class ChannelClientState {
// Handle updates to pinned messages.
final newPinnedMessages = _updatePinnedMessages(message);

// Calculate the new last message at time.
var lastMessageAt = _channelState.channel?.lastMessageAt;
lastMessageAt ??= message.createdAt;
if (_shouldUpdateChannelLastMessageAt(message)) {
lastMessageAt = [lastMessageAt, message.createdAt].max;
}

// Apply the updated lists to the channel state.
_channelState = _channelState.copyWith(
messages: newMessages.sorted(_sortByCreatedAt),
pinnedMessages: newPinnedMessages,
channel: _channelState.channel?.copyWith(
lastMessageAt: message.createdAt,
lastMessageAt: lastMessageAt,
),
);
}
Expand Down
20 changes: 6 additions & 14 deletions packages/stream_chat/lib/src/core/models/attachment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,10 @@ class Attachment extends Equatable {
this.originalHeight,
Map<String, Object?> extraData = const {},
this.file,
UploadState? uploadState,
this.uploadState = const UploadState.preparing(),
}) : id = id ?? const Uuid().v4(),
_type = type,
title = title ?? file?.name,
_uploadState = uploadState,
localUri = file?.path != null ? Uri.parse(file!.path!) : null,
// For backwards compatibility,
// set 'file_size', 'mime_type' in [extraData].
Expand Down Expand Up @@ -97,9 +96,9 @@ class Attachment extends Equatable {
///The attachment type based on the URL resource. This can be: audio,
///image or video
String? get type {
// If the attachment contains titleLink but is not of type giphy, we
// consider it as a urlPreview.
if (_type != AttachmentType.giphy && titleLink != null) {
// If the attachment contains ogScrapeUrl as well as titleLink, we consider
// it as a urlPreview.
if (ogScrapeUrl != null && titleLink != null) {
return AttachmentType.urlPreview;
}

Expand Down Expand Up @@ -163,15 +162,8 @@ class Attachment extends Equatable {
final AttachmentFile? file;

/// The current upload state of the attachment
UploadState get uploadState {
if (_uploadState case final state?) return state;

return ((assetUrl != null || imageUrl != null || thumbUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}

final UploadState? _uploadState;
@JsonKey(defaultValue: UploadState.success)
final UploadState uploadState;

/// Map of custom channel extraData
final Map<String, Object?> extraData;
Expand Down
2 changes: 1 addition & 1 deletion packages/stream_chat/lib/src/core/models/attachment.g.dart

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

8 changes: 8 additions & 0 deletions packages/stream_chat/lib/src/core/models/channel_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ChannelConfig {
this.typingEvents = false,
this.uploads = false,
this.urlEnrichment = false,
this.skipLastMsgUpdateForSystemMsgs = false,
}) : createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now();

Expand Down Expand Up @@ -79,6 +80,13 @@ class ChannelConfig {
/// True if urls appears as attachments
final bool urlEnrichment;

/// If true the last message at date will not be updated when a system message
/// is added.
///
/// This is useful for scenarios where you want to track the last time a user
/// message was added to the channel.
final bool skipLastMsgUpdateForSystemMsgs;

/// Serialize to json
Map<String, dynamic> toJson() => _$ChannelConfigToJson(this);
}

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

7 changes: 7 additions & 0 deletions packages/stream_chat/lib/src/core/models/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Event {
this.member,
this.channelId,
this.channelType,
this.channelLastMessageAt,
this.parentId,
this.hardDelete,
this.aiState,
Expand Down Expand Up @@ -58,6 +59,9 @@ class Event {
/// The channel type to which the event belongs
final String? channelType;

/// The dateTime at which the last message was sent in the channel.
final DateTime? channelLastMessageAt;

/// The connection id in which the event has been sent
final String? connectionId;

Expand Down Expand Up @@ -168,6 +172,7 @@ class Event {
'member',
'channel_id',
'channel_type',
'channel_last_message_at',
'parent_id',
'hard_delete',
'is_local',
Expand All @@ -190,6 +195,7 @@ class Event {
String? cid,
String? channelId,
String? channelType,
DateTime? channelLastMessageAt,
String? connectionId,
DateTime? createdAt,
OwnUser? me,
Expand Down Expand Up @@ -231,6 +237,7 @@ class Event {
member: member ?? this.member,
channelId: channelId ?? this.channelId,
channelType: channelType ?? this.channelType,
channelLastMessageAt: channelLastMessageAt ?? this.channelLastMessageAt,
parentId: parentId ?? this.parentId,
hardDelete: hardDelete ?? this.hardDelete,
aiState: aiState ?? this.aiState,
Expand Down
5 changes: 5 additions & 0 deletions packages/stream_chat/lib/src/core/models/event.g.dart

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

66 changes: 66 additions & 0 deletions packages/stream_chat/lib/src/core/models/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Message extends Equatable {
this.extraData = const {},
this.state = const MessageState.initial(),
this.i18n,
this.restrictedVisibility,
}) : id = id ?? const Uuid().v4(),
pinExpires = pinExpires?.toUtc(),
remoteCreatedAt = createdAt,
Expand Down Expand Up @@ -237,6 +238,14 @@ class Message extends Equatable {
String? get pollId => _pollId ?? poll?.id;
final String? _pollId;

/// The list of user ids that should be able to see the message.
///
/// If null or empty, the message is visible to all users.
/// If populated, only users whose ids are included in this list can see
/// the message.
@JsonKey(includeIfNull: false)
final List<String>? restrictedVisibility;

/// Message custom extraData.
final Map<String, Object?> extraData;

Expand Down Expand Up @@ -291,6 +300,7 @@ class Message extends Equatable {
'i18n',
'poll',
'poll_id',
'restricted_visibility',
];

/// Serialize to json.
Expand Down Expand Up @@ -335,6 +345,7 @@ class Message extends Equatable {
Map<String, Object?>? extraData,
MessageState? state,
Map<String, String>? i18n,
List<String>? restrictedVisibility,
}) {
assert(() {
if (pinExpires is! DateTime &&
Expand Down Expand Up @@ -408,6 +419,7 @@ class Message extends Equatable {
extraData: extraData ?? this.extraData,
state: state ?? this.state,
i18n: i18n ?? this.i18n,
restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility,
);
}

Expand Down Expand Up @@ -450,6 +462,7 @@ class Message extends Equatable {
extraData: other.extraData,
state: other.state,
i18n: other.i18n,
restrictedVisibility: other.restrictedVisibility,
);
}

Expand Down Expand Up @@ -512,5 +525,58 @@ class Message extends Equatable {
extraData,
state,
i18n,
restrictedVisibility,
];
}

/// Extension that adds visibility control functionality to Message objects.
///
/// This extension provides methods to determine if a message is visible to a
/// specific user based on the [Message.restrictedVisibility] list.
extension MessageVisibility on Message {
/// Checks if this message has any visibility restrictions applied.
///
/// Returns true if the restrictedVisibility list exists and contains at
/// least one entry, indicating that visibility of this message is restricted
/// to specific users.
///
/// Returns false if the restrictedVisibility list is null or empty,
/// indicating that this message is visible to all users.
bool get hasRestrictedVisibility {
final visibility = restrictedVisibility;
if (visibility == null || visibility.isEmpty) return false;

return true;
}

/// Determines if a message is visible to a specific user based on
/// restricted visibility settings.
///
/// Returns true in the following cases:
/// - The restrictedVisibility list is null or empty (visible to everyone)
/// - The provided userId is found in the restrictedVisibility list
///
/// Returns false if the restrictedVisibility list exists and doesn't
/// contain the provided userId.
///
/// [userId] The unique identifier of the user to check visibility for.
bool isVisibleTo(String userId) {
final visibility = restrictedVisibility;
if (visibility == null || visibility.isEmpty) return true;

return visibility.contains(userId);
}

/// Determines if a message is not visible to a specific user based on
/// restricted visibility settings.
///
/// Returns true if the restrictedVisibility list exists and doesn't
/// contain the provided userId.
///
/// Returns false in the following cases:
/// - The restrictedVisibility list is null or empty (visible to everyone)
/// - The provided userId is found in the restrictedVisibility list
///
/// [userId] The unique identifier of the user to check visibility for.
bool isNotVisibleTo(String userId) => !isVisibleTo(userId);
}
5 changes: 5 additions & 0 deletions packages/stream_chat/lib/src/core/models/message.g.dart

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

6 changes: 4 additions & 2 deletions packages/stream_chat/test/fixtures/channel_state.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

{
"channel": {
"id": "dev",
Expand Down Expand Up @@ -79,7 +78,10 @@
"pinned_at": null,
"pin_expires": null,
"pinned_by": null,
"poll_id": null
"poll_id": null,
"restricted_visibility": [
"user-id-3"
]
},
{
"id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
"silent": false,
"pinned": false,
"pin_expires": null,
"poll_id": null
"poll_id": null,
"restricted_visibility": [
"user-id-3"
]
},
{
"id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f",
Expand Down
Loading

0 comments on commit 230d083

Please sign in to comment.