Skip to content
Merged
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
2 changes: 0 additions & 2 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ linter:
- control_flow_in_finally
- empty_statements
- hash_and_equals
- invariant_booleans
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
Expand Down Expand Up @@ -81,7 +80,6 @@ linter:
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_contains
- prefer_equal_for_default_values
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
Expand Down
14 changes: 4 additions & 10 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2942,16 +2942,10 @@ class ChannelClientState {
void updateMessage(Message message) {
// Determine if the message should be displayed in the channel view.
if (message.parentId == null || message.showInChannel == true) {
// Locate the existing version (if any). One lookup feeds both the
// quoted-rewrite below and the `sortedUpsertAt` further down — no
// duplicate scan.

final oldIndex = () {
if (messages.isEmpty) return -1;
if (message.createdAt.isAfter(messages.last.createdAt)) return -1;
return messages.indexWhere((it) => it.id == message.id);
}();

// Scan from the tail: server echoes, edits, and reactions almost
// always target a recent message, so `lastIndexWhere` exits in a
// handful of comparisons instead of walking the full list.
final oldIndex = messages.lastIndexWhere((m) => m.id == message.id);
final oldMessage = oldIndex == -1 ? null : messages[oldIndex];

// Carry over local-only timestamps; no-op when there's no prior.
Expand Down
98 changes: 98 additions & 0 deletions packages/stream_chat/test/src/client/channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4192,6 +4192,104 @@ void main() {
).called(1);
},
);

test(
'should not duplicate when server echoes back an optimistically '
'inserted message with a later createdAt',
() async {
// Local message used as the input to `channel.sendMessage`.
final localCreatedAt =
initialLastMessageAt.add(const Duration(seconds: 3));
final localMessage = Message(
id: 'test-message-id',
text: 'Hello world!',
user: client.state.currentUser,
createdAt: localCreatedAt,
);

// Mock the network send to return the message unchanged so the
// optimistic insert + sent-state update both land on the same
// `createdAt`. The bug fires later, on the WS echo.
final sendMessageResponse = SendMessageResponse()
..message = localMessage.copyWith(state: MessageState.sent);
when(() => client.sendMessage(any(), channelId, channelType))
.thenAnswer((_) async => sendMessageResponse);

await channel.sendMessage(localMessage);

expect(channel.state!.messages, hasLength(1));

// Server then broadcasts the same message via a `message.new`
// event with a slightly later `createdAt` (server-assigned
// timestamp).
final serverMessage = localMessage.copyWith(
createdAt: localCreatedAt.add(const Duration(milliseconds: 50)),
);
client.addEvent(createNewMessageEvent(serverMessage));

// Wait for the event to get processed
await Future.delayed(Duration.zero);

// The state should contain exactly one message with that id,
// not a duplicate.
final matching =
channel.state!.messages.where((it) => it.id == localMessage.id);
expect(matching, hasLength(1));
expect(channel.state!.messages, hasLength(1));
},
);

test(
'should not duplicate when the locally-sent message is no longer '
'the latest (retry-after-offline scenario)',
() async {
// Mirrors the offline-retry flow: a local message is sent, then
// another message arrives via WS while the local one is still
// pending. When the retry finally succeeds the server response's
// `createdAt` is later than the intervening message, so the
// locally-sent copy is no longer `messages.last`.
final localCreatedAt =
initialLastMessageAt.add(const Duration(seconds: 1));
final localMessage = Message(
id: 'local-message-id',
text: 'Hello world!',
user: client.state.currentUser,
createdAt: localCreatedAt,
);

final sendMessageResponse = SendMessageResponse()
..message = localMessage.copyWith(state: MessageState.sent);
when(() => client.sendMessage(any(), channelId, channelType))
.thenAnswer((_) async => sendMessageResponse);

await channel.sendMessage(localMessage);

// Another message arrives via WS with a later `createdAt`,
// pushing the locally-sent message off the tail.
final otherMessage = Message(
id: 'other-message-id',
user: User(id: 'other-user'),
createdAt: localCreatedAt.add(const Duration(seconds: 2)),
);
client.addEvent(createNewMessageEvent(otherMessage));
await Future.delayed(Duration.zero);

// Server then broadcasts the locally-sent message via
// `message.new` with a `createdAt` that is later than the
// intervening message — exactly the shape produced by a
// successful retry after another message arrived in between.
final serverEcho = localMessage.copyWith(
createdAt: otherMessage.createdAt.add(const Duration(seconds: 1)),
);
client.addEvent(createNewMessageEvent(serverEcho));
await Future.delayed(Duration.zero);

final localMatches =
channel.state!.messages.where((it) => it.id == localMessage.id);
expect(localMatches, hasLength(1));
expect(channel.state!.messages, hasLength(2));
},
);
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,11 +700,9 @@ class StreamMessageInputState extends State<StreamMessageInput>
final elevation = widget.elevation ?? _messageInputTheme.elevation;
return Material(
elevation: elevation ?? 8,
color: _messageInputTheme.inputBackgroundColor,
child: DecoratedBox(
decoration: BoxDecoration(
color: _messageInputTheme.inputBackgroundColor,
boxShadow: [if (shadow != null) shadow],
),
decoration: BoxDecoration(boxShadow: [if (shadow != null) shadow]),
child: SimpleSafeArea(
enabled: widget.enableSafeArea ?? _messageInputTheme.enableSafeArea,
child: Center(heightFactor: 1, child: messageInput),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ class MessageInputMediaAttachments extends StatelessWidget {
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
// ignore: deprecated_member_use
cacheExtent: 104 * 10, // Cache 10 items ahead.
children: attachments.map<Widget>(
(attachment) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class SeparatedReorderableListView extends ReorderableListView {
super.physics,
super.shrinkWrap,
super.anchor,
// ignore: deprecated_member_use
super.cacheExtent,
super.dragStartBehavior,
super.keyboardDismissBehavior,
Expand Down Expand Up @@ -56,6 +57,7 @@ class SeparatedReorderableListView extends ReorderableListView {

return separator;
},
// ignore: deprecated_member_use
onReorder: (int oldIndex, int newIndex) {
// Adjust the indexes due to an issue in the ReorderableListView
// which isn't going to be fixed in the near future.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,14 @@ class PollSwitchListTile extends StatelessWidget {
final listTile = SwitchListTile(
value: value,
onChanged: onChanged,
tileColor: fillColor,
title: Text(title, style: theme.switchListTileTitleStyle),
contentPadding: const EdgeInsets.only(left: 16, right: 8),
shape: RoundedRectangleBorder(
borderRadius: borderRadius ?? BorderRadius.zero,
),
);

return DecoratedBox(
decoration: BoxDecoration(
color: fillColor,
borderRadius: borderRadius,
),
return Material(
color: fillColor,
borderRadius: borderRadius,
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ class _PagedValueListViewState<K, V> extends State<PagedValueListView<K, V>> {
keyboardDismissBehavior: widget.keyboardDismissBehavior,
restorationId: widget.restorationId,
dragStartBehavior: widget.dragStartBehavior,
// ignore: deprecated_member_use
cacheExtent: widget.cacheExtent,
clipBehavior: widget.clipBehavior,
itemCount: value.itemCount,
Expand Down Expand Up @@ -662,6 +663,7 @@ class _PagedValueGridViewState<K, V> extends State<PagedValueGridView<K, V>> {
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
addSemanticIndexes: widget.addSemanticIndexes,
// ignore: deprecated_member_use
cacheExtent: widget.cacheExtent,
semanticChildCount: widget.semanticChildCount,
dragStartBehavior: widget.dragStartBehavior,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ class StreamChannelState extends State<StreamChannel> {
}

// Find the index of the last read message
final lastReadIndex = messages.indexWhere(
final lastReadIndex = messages.lastIndexWhere(
(message) => message.id == lastReadMessageId,
);

Expand Down
5 changes: 4 additions & 1 deletion sample_app/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ plugins {

android {
namespace "io.getstream.chat.android.flutter.sample"
compileSdkVersion flutter.compileSdkVersion
// Pin a floor of 36 so AAR metadata checks pass for transitive
// dependencies that require it until Flutter's bundled
// compileSdkVersion catches up.
compileSdkVersion Math.max(flutter.compileSdkVersion, 36)
ndkVersion "27.0.12077973"

compileOptions {
Expand Down
3 changes: 1 addition & 2 deletions sample_app/android/app/src/profile/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
Expand Down
7 changes: 5 additions & 2 deletions sample_app/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ subprojects {
android.namespace = project.group
}

// Align the compileSdkVersion with Flutter's compileSdkVersion
android.compileSdkVersion = flutter.compileSdkVersion
// Align the compileSdkVersion with Flutter's compileSdkVersion, but
// pin a floor of 36 so AAR metadata checks pass for transitive
// dependencies that require it until Flutter's bundled
// compileSdkVersion catches up.
android.compileSdkVersion = Math.max(flutter.compileSdkVersion, 36)
}
}

Expand Down
4 changes: 4 additions & 0 deletions sample_app/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ org.gradle.caching=true
org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
2 changes: 0 additions & 2 deletions sample_app/ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>
Loading