Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into feat/hard-delete
Conflicts: packages/stream_chat/CHANGELOG.md
This commit is contained in:
@@ -33,7 +33,7 @@ jobs:
|
|||||||
flutter-version: ${{ env.flutter_version }}
|
flutter-version: ${{ env.flutter_version }}
|
||||||
|
|
||||||
- name: "Install Tools"
|
- name: "Install Tools"
|
||||||
run: flutter pub global activate melos 1.0.0-dev.6
|
run: flutter pub global activate melos 1.0.0-dev.10
|
||||||
- name: "Bootstrap Workspace"
|
- name: "Bootstrap Workspace"
|
||||||
run: melos bootstrap
|
run: melos bootstrap
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
flutter-version: ${{ env.flutter_version }}
|
flutter-version: ${{ env.flutter_version }}
|
||||||
- name: "Install Tools"
|
- name: "Install Tools"
|
||||||
run: |
|
run: |
|
||||||
flutter pub global activate melos 1.0.0-dev.6
|
flutter pub global activate melos 1.0.0-dev.10
|
||||||
- name: "Bootstrap Workspace"
|
- name: "Bootstrap Workspace"
|
||||||
run: melos bootstrap
|
run: melos bootstrap
|
||||||
- name: "Dart Analyze"
|
- name: "Dart Analyze"
|
||||||
|
|||||||
@@ -2,8 +2,15 @@
|
|||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
|
- Extra properties added to `PaginationParams` to aid in fetching messages.
|
||||||
- Added hard delete functionality.
|
- Added hard delete functionality.
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- `closeConnection()` now uses `normalClosure` status when closing websocket.
|
||||||
|
- Fixed local unread count indicator increasing for thread replies
|
||||||
|
- Fixed user presence indicator not updating correctly
|
||||||
|
|
||||||
## 3.2.0
|
## 3.2.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
@@ -726,4 +733,4 @@
|
|||||||
|
|
||||||
## 0.0.2
|
## 0.0.2
|
||||||
|
|
||||||
- first beta version
|
- first beta version
|
||||||
@@ -233,10 +233,10 @@ class _MessageViewState extends State<MessageView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1860,10 +1860,13 @@ class ChannelClientState {
|
|||||||
(m) => m.user.id == message.user?.id,
|
(m) => m.user.id == message.user?.id,
|
||||||
) !=
|
) !=
|
||||||
null;
|
null;
|
||||||
|
final isThreadMessage = message.parentId != null;
|
||||||
|
|
||||||
return !message.silent &&
|
return !message.silent &&
|
||||||
!message.shadowed &&
|
!message.shadowed &&
|
||||||
message.user?.id != userId &&
|
message.user?.id != userId &&
|
||||||
!userIsMuted;
|
!userIsMuted &&
|
||||||
|
!isThreadMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update threads with updated information about messages.
|
/// Update threads with updated information about messages.
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ class PaginationParams extends Equatable {
|
|||||||
/// ```
|
/// ```
|
||||||
const PaginationParams({
|
const PaginationParams({
|
||||||
this.limit = 10,
|
this.limit = 10,
|
||||||
|
this.before = 10,
|
||||||
|
this.after = 10,
|
||||||
this.offset,
|
this.offset,
|
||||||
this.next,
|
this.next,
|
||||||
|
this.idAround,
|
||||||
this.greaterThan,
|
this.greaterThan,
|
||||||
this.greaterThanOrEqual,
|
this.greaterThanOrEqual,
|
||||||
this.lessThan,
|
this.lessThan,
|
||||||
@@ -78,12 +81,22 @@ class PaginationParams extends Equatable {
|
|||||||
/// The amount of items requested from the APIs.
|
/// The amount of items requested from the APIs.
|
||||||
final int limit;
|
final int limit;
|
||||||
|
|
||||||
|
/// The amount of items requested before message ID from the APIs.
|
||||||
|
final int before;
|
||||||
|
|
||||||
|
/// The amount of items requested after message ID from the APIs.
|
||||||
|
final int after;
|
||||||
|
|
||||||
/// The offset of requesting items.
|
/// The offset of requesting items.
|
||||||
final int? offset;
|
final int? offset;
|
||||||
|
|
||||||
/// A key used to paginate.
|
/// A key used to paginate.
|
||||||
final String? next;
|
final String? next;
|
||||||
|
|
||||||
|
/// Message ID to fetch messages around
|
||||||
|
@JsonKey(name: 'id_around')
|
||||||
|
final String? idAround;
|
||||||
|
|
||||||
/// Filter on ids greater than the given value.
|
/// Filter on ids greater than the given value.
|
||||||
@JsonKey(name: 'id_gt')
|
@JsonKey(name: 'id_gt')
|
||||||
final String? greaterThan;
|
final String? greaterThan;
|
||||||
@@ -106,7 +119,10 @@ class PaginationParams extends Equatable {
|
|||||||
/// Creates a copy of [PaginationParams] with specified attributes overridden.
|
/// Creates a copy of [PaginationParams] with specified attributes overridden.
|
||||||
PaginationParams copyWith({
|
PaginationParams copyWith({
|
||||||
int? limit,
|
int? limit,
|
||||||
|
int? before,
|
||||||
|
int? after,
|
||||||
int? offset,
|
int? offset,
|
||||||
|
String? idAround,
|
||||||
String? next,
|
String? next,
|
||||||
String? greaterThan,
|
String? greaterThan,
|
||||||
String? greaterThanOrEqual,
|
String? greaterThanOrEqual,
|
||||||
@@ -115,7 +131,10 @@ class PaginationParams extends Equatable {
|
|||||||
}) =>
|
}) =>
|
||||||
PaginationParams(
|
PaginationParams(
|
||||||
limit: limit ?? this.limit,
|
limit: limit ?? this.limit,
|
||||||
|
before: before ?? this.before,
|
||||||
|
after: limit ?? this.after,
|
||||||
offset: offset ?? this.offset,
|
offset: offset ?? this.offset,
|
||||||
|
idAround: idAround ?? this.idAround,
|
||||||
next: next ?? this.next,
|
next: next ?? this.next,
|
||||||
greaterThan: greaterThan ?? this.greaterThan,
|
greaterThan: greaterThan ?? this.greaterThan,
|
||||||
greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual,
|
greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual,
|
||||||
@@ -126,8 +145,11 @@ class PaginationParams extends Equatable {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
limit,
|
limit,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
offset,
|
offset,
|
||||||
next,
|
next,
|
||||||
|
idAround,
|
||||||
greaterThan,
|
greaterThan,
|
||||||
greaterThanOrEqual,
|
greaterThanOrEqual,
|
||||||
lessThan,
|
lessThan,
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
|
|||||||
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
||||||
PaginationParams(
|
PaginationParams(
|
||||||
limit: json['limit'] as int? ?? 10,
|
limit: json['limit'] as int? ?? 10,
|
||||||
|
before: json['before'] as int? ?? 10,
|
||||||
|
after: json['after'] as int? ?? 10,
|
||||||
offset: json['offset'] as int?,
|
offset: json['offset'] as int?,
|
||||||
next: json['next'] as String?,
|
next: json['next'] as String?,
|
||||||
|
idAround: json['id_around'] as String?,
|
||||||
greaterThan: json['id_gt'] as String?,
|
greaterThan: json['id_gt'] as String?,
|
||||||
greaterThanOrEqual: json['id_gte'] as String?,
|
greaterThanOrEqual: json['id_gte'] as String?,
|
||||||
lessThan: json['id_lt'] as String?,
|
lessThan: json['id_lt'] as String?,
|
||||||
@@ -32,6 +35,8 @@ PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
|||||||
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
||||||
final val = <String, dynamic>{
|
final val = <String, dynamic>{
|
||||||
'limit': instance.limit,
|
'limit': instance.limit,
|
||||||
|
'before': instance.before,
|
||||||
|
'after': instance.after,
|
||||||
};
|
};
|
||||||
|
|
||||||
void writeNotNull(String key, dynamic value) {
|
void writeNotNull(String key, dynamic value) {
|
||||||
@@ -42,6 +47,7 @@ Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
|||||||
|
|
||||||
writeNotNull('offset', instance.offset);
|
writeNotNull('offset', instance.offset);
|
||||||
writeNotNull('next', instance.next);
|
writeNotNull('next', instance.next);
|
||||||
|
writeNotNull('id_around', instance.idAround);
|
||||||
writeNotNull('id_gt', instance.greaterThan);
|
writeNotNull('id_gt', instance.greaterThan);
|
||||||
writeNotNull('id_gte', instance.greaterThanOrEqual);
|
writeNotNull('id_gte', instance.greaterThanOrEqual);
|
||||||
writeNotNull('id_lt', instance.lessThan);
|
writeNotNull('id_lt', instance.lessThan);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:stream_chat/src/core/models/user.dart';
|
import 'package:stream_chat/src/core/models/user.dart';
|
||||||
|
|
||||||
@@ -5,7 +6,7 @@ part 'read.g.dart';
|
|||||||
|
|
||||||
/// The class that defines a read event
|
/// The class that defines a read event
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Read {
|
class Read extends Equatable {
|
||||||
/// Constructor used for json serialization
|
/// Constructor used for json serialization
|
||||||
Read({
|
Read({
|
||||||
required this.lastRead,
|
required this.lastRead,
|
||||||
@@ -39,4 +40,11 @@ class Read {
|
|||||||
user: user ?? this.user,
|
user: user ?? this.user,
|
||||||
unreadMessages: unreadMessages ?? this.unreadMessages,
|
unreadMessages: unreadMessages ?? this.unreadMessages,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
lastRead,
|
||||||
|
user,
|
||||||
|
unreadMessages,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,5 +179,14 @@ class User extends Equatable {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [id, role];
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
lastActive,
|
||||||
|
online,
|
||||||
|
extraData,
|
||||||
|
banned,
|
||||||
|
teams,
|
||||||
|
language,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ class WebSocket with TimerHelper {
|
|||||||
_logger?.info('Closing connection with $baseUrl');
|
_logger?.info('Closing connection with $baseUrl');
|
||||||
if (_webSocketChannel != null) {
|
if (_webSocketChannel != null) {
|
||||||
_unsubscribeFromWebSocketChannel();
|
_unsubscribeFromWebSocketChannel();
|
||||||
_webSocketChannel?.sink.close(status.goingAway);
|
_webSocketChannel?.sink
|
||||||
|
.close(_manuallyClosed ? status.normalClosure : status.goingAway);
|
||||||
_webSocketChannel = null;
|
_webSocketChannel = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,13 +70,19 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
newReaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'});
|
newReaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'});
|
||||||
|
|
||||||
|
final newUserCreateTime = DateTime.now();
|
||||||
|
|
||||||
newReaction = reaction.copyWith(
|
newReaction = reaction.copyWith(
|
||||||
type: 'lol',
|
type: 'lol',
|
||||||
createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'),
|
createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'),
|
||||||
extraData: {},
|
extraData: {},
|
||||||
messageId: 'test',
|
messageId: 'test',
|
||||||
score: 2,
|
score: 2,
|
||||||
user: User(id: 'test'),
|
user: User(
|
||||||
|
id: 'test',
|
||||||
|
createdAt: newUserCreateTime,
|
||||||
|
updatedAt: newUserCreateTime,
|
||||||
|
),
|
||||||
userId: 'test',
|
userId: 'test',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -88,12 +94,21 @@ void main() {
|
|||||||
expect(newReaction.extraData, {});
|
expect(newReaction.extraData, {});
|
||||||
expect(newReaction.messageId, 'test');
|
expect(newReaction.messageId, 'test');
|
||||||
expect(newReaction.score, 2);
|
expect(newReaction.score, 2);
|
||||||
expect(newReaction.user, User(id: 'test'));
|
expect(
|
||||||
|
newReaction.user,
|
||||||
|
User(
|
||||||
|
id: 'test',
|
||||||
|
createdAt: newUserCreateTime,
|
||||||
|
updatedAt: newUserCreateTime,
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(newReaction.userId, 'test');
|
expect(newReaction.userId, 'test');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('merge', () {
|
test('merge', () {
|
||||||
final reaction = Reaction.fromJson(jsonFixture('reaction.json'));
|
final reaction = Reaction.fromJson(jsonFixture('reaction.json'));
|
||||||
|
final newUserCreateTime = DateTime.now();
|
||||||
|
|
||||||
final newReaction = reaction.merge(
|
final newReaction = reaction.merge(
|
||||||
Reaction(
|
Reaction(
|
||||||
type: 'lol',
|
type: 'lol',
|
||||||
@@ -101,7 +116,11 @@ void main() {
|
|||||||
extraData: {},
|
extraData: {},
|
||||||
messageId: 'test',
|
messageId: 'test',
|
||||||
score: 2,
|
score: 2,
|
||||||
user: User(id: 'test'),
|
user: User(
|
||||||
|
id: 'test',
|
||||||
|
createdAt: newUserCreateTime,
|
||||||
|
updatedAt: newUserCreateTime,
|
||||||
|
),
|
||||||
userId: 'test',
|
userId: 'test',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -114,7 +133,14 @@ void main() {
|
|||||||
expect(newReaction.extraData, {});
|
expect(newReaction.extraData, {});
|
||||||
expect(newReaction.messageId, 'test');
|
expect(newReaction.messageId, 'test');
|
||||||
expect(newReaction.score, 2);
|
expect(newReaction.score, 2);
|
||||||
expect(newReaction.user, User(id: 'test'));
|
expect(
|
||||||
|
newReaction.user,
|
||||||
|
User(
|
||||||
|
id: 'test',
|
||||||
|
createdAt: newUserCreateTime,
|
||||||
|
updatedAt: newUserCreateTime,
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(newReaction.userId, 'test');
|
expect(newReaction.userId, 'test');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
|
## Upcoming
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- `MessageListView` now allows more better control over spacing after messages using `spacingWidgetBuilder`.
|
||||||
|
- `StreamChannel` can now fetch messages around a message ID with the `queryAroundMessage` call.
|
||||||
|
- Added `MessageListView.keyboardDismissBehavior` property.
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#766]]`AttachmentActionsModal` now has customisation options for actions.
|
||||||
|
- Fixed `MessageWidget` null errors associated with `channel.memberCount`.
|
||||||
|
- Fixed adding attachments on web.
|
||||||
|
- [[#767]](https://github.com/GetStream/stream-chat-flutter/issues/767): Fix `MessageInput` focus behaviour when sending messages.
|
||||||
|
- Fixed user presence indicator not updating correctly.
|
||||||
|
- Do not use `withData: true` in `FilePicker` calls.
|
||||||
|
- Fixed read indicator not updating correctly in specific situations.
|
||||||
|
|
||||||
## 3.2.0
|
## 3.2.0
|
||||||
|
|
||||||
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`
|
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`.
|
||||||
- Updated `stream_chat_flutter_core` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
- Updated `stream_chat_flutter_core` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|
||||||
- Fixed message highlight animation alignment in `MessageListView`
|
- Fixed message highlight animation alignment in `MessageListView`.
|
||||||
- [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order.
|
- [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order.
|
||||||
- Fixed `MessageListView` initialIndex not working in some cases.
|
- Fixed `MessageListView` initialIndex not working in some cases.
|
||||||
- Improved `MessageListView` rendering in case of reordering.
|
- Improved `MessageListView` rendering in case of reordering.
|
||||||
- Fix image thumbnail generation when using Stream CDN
|
- Fix image thumbnail generation when using Stream CDN.
|
||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
@@ -860,4 +878,4 @@ The property showVideoFullScreen was added mainly because of this issue brianega
|
|||||||
|
|
||||||
## 0.0.1
|
## 0.0.1
|
||||||
|
|
||||||
- First release
|
- First release
|
||||||
@@ -64,11 +64,12 @@ class MyApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
messageListViewTheme: const MessageListViewThemeData(
|
messageListViewTheme: const MessageListViewThemeData(
|
||||||
backgroundColor: Colors.grey,
|
backgroundColor: Colors.grey,
|
||||||
backgroundImage: DecorationImage(
|
backgroundImage: DecorationImage(
|
||||||
image: AssetImage('assets/background_doodle.png'),
|
image: AssetImage('assets/background_doodle.png'),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
otherMessageTheme: MessageThemeData(
|
otherMessageTheme: MessageThemeData(
|
||||||
messageBackgroundColor: colorTheme.textHighEmphasis,
|
messageBackgroundColor: colorTheme.textHighEmphasis,
|
||||||
messageTextStyle: TextStyle(
|
messageTextStyle: TextStyle(
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class PositionedList extends StatefulWidget {
|
|||||||
this.addSemanticIndexes = true,
|
this.addSemanticIndexes = true,
|
||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.addAutomaticKeepAlives = true,
|
this.addAutomaticKeepAlives = true,
|
||||||
|
this.keyboardDismissBehavior,
|
||||||
}) : assert((positionedIndex == 0) || (positionedIndex < itemCount),
|
}) : assert((positionedIndex == 0) || (positionedIndex < itemCount),
|
||||||
'positionedIndex cannot be 0 and must be smaller than itemCount'),
|
'positionedIndex cannot be 0 and must be smaller than itemCount'),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@@ -134,6 +135,10 @@ class PositionedList extends StatefulWidget {
|
|||||||
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
||||||
final bool addAutomaticKeepAlives;
|
final bool addAutomaticKeepAlives;
|
||||||
|
|
||||||
|
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
|
||||||
|
/// dismiss the keyboard automatically.
|
||||||
|
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _PositionedListState();
|
State<StatefulWidget> createState() => _PositionedListState();
|
||||||
}
|
}
|
||||||
@@ -173,6 +178,7 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
anchor: widget.alignment,
|
anchor: widget.alignment,
|
||||||
center: _centerKey,
|
center: _centerKey,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
cacheExtent: widget.cacheExtent,
|
cacheExtent: widget.cacheExtent,
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ class UnboundedCustomScrollView extends CustomScrollView {
|
|||||||
List<Widget> slivers = const <Widget>[],
|
List<Widget> slivers = const <Widget>[],
|
||||||
int? semanticChildCount,
|
int? semanticChildCount,
|
||||||
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
|
||||||
|
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior,
|
||||||
}) : _anchor = anchor,
|
}) : _anchor = anchor,
|
||||||
super(
|
super(
|
||||||
key: key,
|
key: key,
|
||||||
|
keyboardDismissBehavior: keyboardDismissBehavior ??
|
||||||
|
ScrollViewKeyboardDismissBehavior.manual,
|
||||||
scrollDirection: scrollDirection,
|
scrollDirection: scrollDirection,
|
||||||
reverse: reverse,
|
reverse: reverse,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|||||||
+9
@@ -52,6 +52,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.minCacheExtent,
|
this.minCacheExtent,
|
||||||
this.findChildIndexCallback,
|
this.findChildIndexCallback,
|
||||||
|
this.keyboardDismissBehavior,
|
||||||
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
||||||
separatorBuilder = null,
|
separatorBuilder = null,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@@ -77,6 +78,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.minCacheExtent,
|
this.minCacheExtent,
|
||||||
this.findChildIndexCallback,
|
this.findChildIndexCallback,
|
||||||
|
this.keyboardDismissBehavior,
|
||||||
}) : assert(separatorBuilder != null, 'seperatorBuilder cannot be null'),
|
}) : assert(separatorBuilder != null, 'seperatorBuilder cannot be null'),
|
||||||
itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@@ -92,6 +94,10 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
/// index of the child element with that associated key, or null if not found.
|
/// index of the child element with that associated key, or null if not found.
|
||||||
final ChildIndexGetter? findChildIndexCallback;
|
final ChildIndexGetter? findChildIndexCallback;
|
||||||
|
|
||||||
|
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
|
||||||
|
/// dismiss the keyboard automatically.
|
||||||
|
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
|
||||||
|
|
||||||
/// Number of items the [itemBuilder] can produce.
|
/// Number of items the [itemBuilder] can produce.
|
||||||
final int itemCount;
|
final int itemCount;
|
||||||
|
|
||||||
@@ -344,6 +350,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
child: NotificationListener<ScrollNotification>(
|
child: NotificationListener<ScrollNotification>(
|
||||||
onNotification: (_) => _isTransitioning,
|
onNotification: (_) => _isTransitioning,
|
||||||
child: PositionedList(
|
child: PositionedList(
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
itemBuilder: widget.itemBuilder,
|
itemBuilder: widget.itemBuilder,
|
||||||
separatorBuilder: widget.separatorBuilder,
|
separatorBuilder: widget.separatorBuilder,
|
||||||
itemCount: widget.itemCount,
|
itemCount: widget.itemCount,
|
||||||
@@ -374,6 +381,8 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
child: NotificationListener<ScrollNotification>(
|
child: NotificationListener<ScrollNotification>(
|
||||||
onNotification: (_) => false,
|
onNotification: (_) => false,
|
||||||
child: PositionedList(
|
child: PositionedList(
|
||||||
|
keyboardDismissBehavior:
|
||||||
|
widget.keyboardDismissBehavior,
|
||||||
itemBuilder: widget.itemBuilder,
|
itemBuilder: widget.itemBuilder,
|
||||||
separatorBuilder: widget.separatorBuilder,
|
separatorBuilder: widget.separatorBuilder,
|
||||||
itemCount: widget.itemCount,
|
itemCount: widget.itemCount,
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
this.imageDownloader,
|
this.imageDownloader,
|
||||||
this.fileDownloader,
|
this.fileDownloader,
|
||||||
|
this.showReply = true,
|
||||||
|
this.showShowInChat = true,
|
||||||
|
this.showSave = true,
|
||||||
|
this.showDelete = true,
|
||||||
|
this.customActions = const [],
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// The message containing the attachments
|
/// The message containing the attachments
|
||||||
@@ -41,6 +46,49 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
/// Callback to provide download files
|
/// Callback to provide download files
|
||||||
final AttachmentDownloader? fileDownloader;
|
final AttachmentDownloader? fileDownloader;
|
||||||
|
|
||||||
|
/// Show reply option
|
||||||
|
final bool showReply;
|
||||||
|
|
||||||
|
/// Show show in chat option
|
||||||
|
final bool showShowInChat;
|
||||||
|
|
||||||
|
/// Show save option
|
||||||
|
final bool showSave;
|
||||||
|
|
||||||
|
/// Show delete option
|
||||||
|
final bool showDelete;
|
||||||
|
|
||||||
|
/// List of custom actions
|
||||||
|
final List<AttachmentAction> customActions;
|
||||||
|
|
||||||
|
/// Creates a copy of [MessageWidget] with specified attributes overridden.
|
||||||
|
AttachmentActionsModal copyWith({
|
||||||
|
Key? key,
|
||||||
|
int? currentIndex,
|
||||||
|
Message? message,
|
||||||
|
VoidCallback? onShowMessage,
|
||||||
|
AttachmentDownloader? imageDownloader,
|
||||||
|
AttachmentDownloader? fileDownloader,
|
||||||
|
bool? showReply,
|
||||||
|
bool? showShowInChat,
|
||||||
|
bool? showSave,
|
||||||
|
bool? showDelete,
|
||||||
|
List<AttachmentAction>? customActions,
|
||||||
|
}) =>
|
||||||
|
AttachmentActionsModal(
|
||||||
|
key: key ?? this.key,
|
||||||
|
currentIndex: currentIndex ?? this.currentIndex,
|
||||||
|
message: message ?? this.message,
|
||||||
|
onShowMessage: onShowMessage ?? this.onShowMessage,
|
||||||
|
imageDownloader: imageDownloader ?? this.imageDownloader,
|
||||||
|
fileDownloader: fileDownloader ?? this.fileDownloader,
|
||||||
|
showReply: showReply ?? this.showReply,
|
||||||
|
showShowInChat: showShowInChat ?? this.showShowInChat,
|
||||||
|
showSave: showSave ?? this.showSave,
|
||||||
|
showDelete: showDelete ?? this.showDelete,
|
||||||
|
customActions: customActions ?? this.customActions,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => GestureDetector(
|
Widget build(BuildContext context) => GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
@@ -67,82 +115,86 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildButton(
|
if (showReply)
|
||||||
context,
|
_buildButton(
|
||||||
context.translations.replyLabel,
|
context,
|
||||||
StreamSvgIcon.iconCurveLineLeftUp(
|
context.translations.replyLabel,
|
||||||
size: 24,
|
StreamSvgIcon.iconCurveLineLeftUp(
|
||||||
color: theme.colorTheme.textLowEmphasis,
|
size: 24,
|
||||||
|
color: theme.colorTheme.textLowEmphasis,
|
||||||
|
),
|
||||||
|
() {
|
||||||
|
Navigator.pop(context, ReturnActionType.reply);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
() {
|
if (showShowInChat)
|
||||||
Navigator.pop(context, ReturnActionType.reply);
|
_buildButton(
|
||||||
},
|
context,
|
||||||
),
|
context.translations.showInChatLabel,
|
||||||
_buildButton(
|
StreamSvgIcon.eye(
|
||||||
context,
|
size: 24,
|
||||||
context.translations.showInChatLabel,
|
color: theme.colorTheme.textHighEmphasis,
|
||||||
StreamSvgIcon.eye(
|
),
|
||||||
size: 24,
|
onShowMessage,
|
||||||
color: theme.colorTheme.textHighEmphasis,
|
|
||||||
),
|
),
|
||||||
onShowMessage,
|
if (showSave)
|
||||||
),
|
_buildButton(
|
||||||
_buildButton(
|
context,
|
||||||
context,
|
message.attachments[currentIndex].type == 'video'
|
||||||
message.attachments[currentIndex].type == 'video'
|
? context.translations.saveVideoLabel
|
||||||
? context.translations.saveVideoLabel
|
: context.translations.saveImageLabel,
|
||||||
: context.translations.saveImageLabel,
|
StreamSvgIcon.iconSave(
|
||||||
StreamSvgIcon.iconSave(
|
size: 24,
|
||||||
size: 24,
|
color: theme.colorTheme.textLowEmphasis,
|
||||||
color: theme.colorTheme.textLowEmphasis,
|
),
|
||||||
|
() {
|
||||||
|
final attachment = message.attachments[currentIndex];
|
||||||
|
final isImage = attachment.type == 'image';
|
||||||
|
final Future<String?> Function(
|
||||||
|
Attachment, {
|
||||||
|
void Function(int, int) progressCallback,
|
||||||
|
}) saveFile = fileDownloader ?? _downloadAttachment;
|
||||||
|
final Future<String?> Function(
|
||||||
|
Attachment, {
|
||||||
|
void Function(int, int) progressCallback,
|
||||||
|
}) saveImage = imageDownloader ?? _downloadAttachment;
|
||||||
|
final downloader = isImage ? saveImage : saveFile;
|
||||||
|
|
||||||
|
final progressNotifier =
|
||||||
|
ValueNotifier<_DownloadProgress?>(
|
||||||
|
_DownloadProgress.initial(),
|
||||||
|
);
|
||||||
|
|
||||||
|
downloader(
|
||||||
|
attachment,
|
||||||
|
progressCallback: (received, total) {
|
||||||
|
progressNotifier.value = _DownloadProgress(
|
||||||
|
total,
|
||||||
|
received,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).catchError((e, stk) {
|
||||||
|
progressNotifier.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Closing attachment actions modal before opening
|
||||||
|
// attachment download dialog
|
||||||
|
Navigator.pop(context);
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
barrierDismissible: false,
|
||||||
|
context: context,
|
||||||
|
barrierColor: theme.colorTheme.overlay,
|
||||||
|
builder: (context) => _buildDownloadProgressDialog(
|
||||||
|
context,
|
||||||
|
progressNotifier,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
() {
|
|
||||||
final attachment = message.attachments[currentIndex];
|
|
||||||
final isImage = attachment.type == 'image';
|
|
||||||
final Future<String?> Function(
|
|
||||||
Attachment, {
|
|
||||||
void Function(int, int) progressCallback,
|
|
||||||
}) saveFile = fileDownloader ?? _downloadAttachment;
|
|
||||||
final Future<String?> Function(
|
|
||||||
Attachment, {
|
|
||||||
void Function(int, int) progressCallback,
|
|
||||||
}) saveImage = imageDownloader ?? _downloadAttachment;
|
|
||||||
final downloader = isImage ? saveImage : saveFile;
|
|
||||||
|
|
||||||
final progressNotifier =
|
|
||||||
ValueNotifier<_DownloadProgress?>(
|
|
||||||
_DownloadProgress.initial(),
|
|
||||||
);
|
|
||||||
|
|
||||||
downloader(
|
|
||||||
attachment,
|
|
||||||
progressCallback: (received, total) {
|
|
||||||
progressNotifier.value = _DownloadProgress(
|
|
||||||
total,
|
|
||||||
received,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).catchError((e, stk) {
|
|
||||||
progressNotifier.value = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Closing attachment actions modal before opening
|
|
||||||
// attachment download dialog
|
|
||||||
Navigator.pop(context);
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
barrierDismissible: false,
|
|
||||||
context: context,
|
|
||||||
barrierColor: theme.colorTheme.overlay,
|
|
||||||
builder: (context) => _buildDownloadProgressDialog(
|
|
||||||
context,
|
|
||||||
progressNotifier,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (StreamChat.of(context).currentUser?.id ==
|
if (StreamChat.of(context).currentUser?.id ==
|
||||||
message.user?.id)
|
message.user?.id &&
|
||||||
|
showDelete)
|
||||||
_buildButton(
|
_buildButton(
|
||||||
context,
|
context,
|
||||||
context.translations.deleteLabel.capitalize(),
|
context.translations.deleteLabel.capitalize(),
|
||||||
@@ -171,6 +223,16 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
color: theme.colorTheme.accentError,
|
color: theme.colorTheme.accentError,
|
||||||
),
|
),
|
||||||
|
...customActions
|
||||||
|
.map(
|
||||||
|
(e) => _buildButton(
|
||||||
|
context,
|
||||||
|
e.actionTitle,
|
||||||
|
e.icon,
|
||||||
|
e.onTap,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
]
|
]
|
||||||
.map<Widget>((e) => Align(
|
.map<Widget>((e) => Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@@ -193,7 +255,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
|||||||
Widget _buildButton(
|
Widget _buildButton(
|
||||||
context,
|
context,
|
||||||
String title,
|
String title,
|
||||||
StreamSvgIcon icon,
|
Widget icon,
|
||||||
VoidCallback? onTap, {
|
VoidCallback? onTap, {
|
||||||
Color? color,
|
Color? color,
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -331,3 +393,22 @@ class _DownloadProgress {
|
|||||||
|
|
||||||
int get toPercentage => (received * 100) ~/ total;
|
int get toPercentage => (received * 100) ~/ total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Class for custom attachment action
|
||||||
|
class AttachmentAction {
|
||||||
|
/// Constructor for custom attachment action
|
||||||
|
AttachmentAction({
|
||||||
|
required this.actionTitle,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Title for the attachment action
|
||||||
|
String actionTitle;
|
||||||
|
|
||||||
|
/// Icon for the attachment action
|
||||||
|
Widget icon;
|
||||||
|
|
||||||
|
/// Callback for when the action is tapped
|
||||||
|
VoidCallback onTap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,9 +59,10 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
final memberCount = channel.memberCount;
|
final memberCount = channel.memberCount;
|
||||||
if (memberCount != null && memberCount > 2) {
|
if (memberCount != null && memberCount > 2) {
|
||||||
var text = context.translations.membersCountText(memberCount);
|
var text = context.translations.membersCountText(memberCount);
|
||||||
final watcherCount = channel.state?.watcherCount ?? 0;
|
final onlineCount =
|
||||||
if (watcherCount > 0) {
|
members?.where((m) => m.user?.online == true).length ?? 0;
|
||||||
text += ' ${context.translations.watchersCountText(watcherCount)}';
|
if (onlineCount > 0) {
|
||||||
|
text += ', ${context.translations.watchersCountText(onlineCount)}';
|
||||||
}
|
}
|
||||||
alternativeWidget = Text(
|
alternativeWidget = Text(
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -126,16 +126,26 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
streamChatState.currentUser?.id) {
|
streamChatState.currentUser?.id) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 4),
|
padding: const EdgeInsets.only(right: 4),
|
||||||
child: SendingIndicator(
|
child: BetterStreamBuilder<List<Read>>(
|
||||||
message: lastMessage!,
|
stream: channel.state?.readStream,
|
||||||
size: channelPreviewTheme.indicatorIconSize,
|
initialData: channel.state?.read,
|
||||||
isMessageRead: channel.state!.read
|
builder: (context, data) {
|
||||||
.where((element) =>
|
final readList = data.where((it) =>
|
||||||
element.user.id !=
|
it.user.id !=
|
||||||
channel.client.state.currentUser!.id)
|
channel.client.state.currentUser?.id &&
|
||||||
.where((element) => element.lastRead
|
(it.lastRead
|
||||||
.isAfter(lastMessage.createdAt))
|
.isAfter(lastMessage!.createdAt) ||
|
||||||
.isNotEmpty,
|
it.lastRead.isAtSameMomentAs(
|
||||||
|
lastMessage.createdAt,
|
||||||
|
)));
|
||||||
|
final isMessageRead = readList.length >=
|
||||||
|
(channel.memberCount ?? 0) - 1;
|
||||||
|
return SendingIndicator(
|
||||||
|
message: lastMessage!,
|
||||||
|
size: channelPreviewTheme.indicatorIconSize,
|
||||||
|
isMessageRead: isMessageRead,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:characters/characters.dart';
|
import 'package:characters/characters.dart';
|
||||||
import 'package:diacritic/diacritic.dart';
|
import 'package:diacritic/diacritic.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||||
import 'package:stream_chat_flutter/src/localization/translations.dart';
|
import 'package:stream_chat_flutter/src/localization/translations.dart';
|
||||||
@@ -46,7 +47,7 @@ extension IterableX<T> on Iterable<T> {
|
|||||||
extension PlatformFileX on PlatformFile {
|
extension PlatformFileX on PlatformFile {
|
||||||
/// Converts the [PlatformFile] into [AttachmentFile]
|
/// Converts the [PlatformFile] into [AttachmentFile]
|
||||||
AttachmentFile get toAttachmentFile => AttachmentFile(
|
AttachmentFile get toAttachmentFile => AttachmentFile(
|
||||||
path: path,
|
path: kIsWeb ? null : path,
|
||||||
name: name,
|
name: name,
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
size: size,
|
size: size,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class FullScreenMedia extends StatefulWidget {
|
|||||||
this.startIndex = 0,
|
this.startIndex = 0,
|
||||||
String? userName,
|
String? userName,
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
|
this.attachmentActionsModalBuilder,
|
||||||
}) : userName = userName ?? '',
|
}) : userName = userName ?? '',
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@@ -51,6 +52,11 @@ class FullScreenMedia extends StatefulWidget {
|
|||||||
/// Callback for when show message is tapped
|
/// Callback for when show message is tapped
|
||||||
final ShowMessageCallback? onShowMessage;
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
|
/// Widget builder for attachment actions modal
|
||||||
|
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
|
||||||
|
/// Use [defaultActionsModal.copyWith] to easily customize it
|
||||||
|
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_FullScreenMediaState createState() => _FullScreenMediaState();
|
_FullScreenMediaState createState() => _FullScreenMediaState();
|
||||||
}
|
}
|
||||||
@@ -196,6 +202,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
StreamChannel.of(context).channel,
|
StreamChannel.of(context).channel,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
attachmentActionsModalBuilder:
|
||||||
|
widget.attachmentActionsModalBuilder,
|
||||||
),
|
),
|
||||||
if (!widget.message.isEphemeral)
|
if (!widget.message.isEphemeral)
|
||||||
GalleryFooter(
|
GalleryFooter(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|||||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
|
/// Widget builder for attachment actions modal
|
||||||
|
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
|
||||||
|
/// Use [defaultActionsModal.copyWith] to easily customize it
|
||||||
|
typedef AttachmentActionsBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
Attachment attachment,
|
||||||
|
AttachmentActionsModal defaultActionsModal,
|
||||||
|
);
|
||||||
|
|
||||||
/// Header/AppBar widget for media display screen
|
/// Header/AppBar widget for media display screen
|
||||||
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
/// Creates a channel header
|
/// Creates a channel header
|
||||||
@@ -21,6 +30,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.userName = '',
|
this.userName = '',
|
||||||
this.sentAt = '',
|
this.sentAt = '',
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
|
this.attachmentActionsModalBuilder,
|
||||||
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@@ -55,6 +65,11 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// The background color of this [GalleryHeader].
|
/// The background color of this [GalleryHeader].
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
/// Widget builder for attachment actions modal
|
||||||
|
/// [defaultActionsModal] is the default [AttachmentActionsModal] config
|
||||||
|
/// Use [defaultActionsModal.copyWith] to easily customize it
|
||||||
|
final AttachmentActionsBuilder? attachmentActionsModalBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
|
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
|
||||||
@@ -123,17 +138,26 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final galleryHeaderThemeData =
|
final galleryHeaderThemeData =
|
||||||
StreamChatTheme.of(context).galleryHeaderTheme;
|
StreamChatTheme.of(context).galleryHeaderTheme;
|
||||||
|
|
||||||
|
final defaultModal = AttachmentActionsModal(
|
||||||
|
message: message,
|
||||||
|
currentIndex: currentIndex,
|
||||||
|
onShowMessage: onShowMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
final effectiveModal = attachmentActionsModalBuilder?.call(
|
||||||
|
context,
|
||||||
|
message.attachments[currentIndex],
|
||||||
|
defaultModal,
|
||||||
|
) ??
|
||||||
|
defaultModal;
|
||||||
|
|
||||||
final result = await showDialog(
|
final result = await showDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
|
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
|
||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: AttachmentActionsModal(
|
child: effectiveModal,
|
||||||
message: message,
|
|
||||||
currentIndex: currentIndex,
|
|
||||||
onShowMessage: onShowMessage,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class GroupAvatar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
initialData: member,
|
initialData: member,
|
||||||
builder: (context, member) => UserAvatar(
|
builder: (context, member) => UserAvatar(
|
||||||
|
showOnlineStatus: false,
|
||||||
user: member.user!,
|
user: member.user!,
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
),
|
),
|
||||||
@@ -118,6 +119,7 @@ class GroupAvatar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
initialData: member,
|
initialData: member,
|
||||||
builder: (context, member) => UserAvatar(
|
builder: (context, member) => UserAvatar(
|
||||||
|
showOnlineStatus: false,
|
||||||
user: member.user!,
|
user: member.user!,
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.commandButtonBuilder,
|
this.commandButtonBuilder,
|
||||||
this.customOverlays = const [],
|
this.customOverlays = const [],
|
||||||
this.mentionAllAppUsers = false,
|
this.mentionAllAppUsers = false,
|
||||||
|
this.shouldKeepFocusAfterMessage,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
initialMessage == null || editMessage == null,
|
initialMessage == null || editMessage == null,
|
||||||
"Can't provide both `initialMessage` and `editMessage`",
|
"Can't provide both `initialMessage` and `editMessage`",
|
||||||
@@ -322,6 +323,10 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// Defaults to false.
|
/// Defaults to false.
|
||||||
final bool mentionAllAppUsers;
|
final bool mentionAllAppUsers;
|
||||||
|
|
||||||
|
/// Defines if the [MessageInput] loses focuses after a message is sent.
|
||||||
|
/// The default behaviour keeps focus until a command is enabled.
|
||||||
|
final bool? shouldKeepFocusAfterMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MessageInputState createState() => MessageInputState();
|
MessageInputState createState() => MessageInputState();
|
||||||
|
|
||||||
@@ -1640,7 +1645,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
final res = await FilePicker.platform.pickFiles(
|
final res = await FilePicker.platform.pickFiles(
|
||||||
type: type,
|
type: type,
|
||||||
withData: true,
|
|
||||||
);
|
);
|
||||||
if (res?.files.isNotEmpty == true) {
|
if (res?.files.isNotEmpty == true) {
|
||||||
file = res!.files.single.toAttachmentFile;
|
file = res!.files.single.toAttachmentFile;
|
||||||
@@ -1759,7 +1763,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final shouldUnfocus = _commandEnabled;
|
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
|
||||||
|
|
||||||
|
shouldKeepFocus ??= !_commandEnabled;
|
||||||
|
|
||||||
if (_commandEnabled) {
|
if (_commandEnabled) {
|
||||||
text = '${'/${_chosenCommand!.name} '}$text';
|
text = '${'/${_chosenCommand!.name} '}$text';
|
||||||
@@ -1822,8 +1828,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
sendingFuture = channel.updateMessage(message);
|
sendingFuture = channel.updateMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldUnfocus) {
|
if (shouldKeepFocus) {
|
||||||
FocusScope.of(context).requestFocus(_focusNode);
|
FocusScope.of(context).requestFocus(_focusNode);
|
||||||
|
} else {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
final resp = await sendingFuture;
|
final resp = await sendingFuture;
|
||||||
|
|||||||
@@ -58,6 +58,46 @@ typedef OnMessageTap = void Function(Message);
|
|||||||
/// Callback on reply tapped
|
/// Callback on reply tapped
|
||||||
typedef ReplyTapCallback = void Function(Message);
|
typedef ReplyTapCallback = void Function(Message);
|
||||||
|
|
||||||
|
/// Spacing Types (These are properties of a message to help inform the decision
|
||||||
|
/// of how much space / which widget to build after it)
|
||||||
|
enum SpacingType {
|
||||||
|
/// Message is a thread
|
||||||
|
thread,
|
||||||
|
|
||||||
|
/// There is a >1s time diff between current and last message
|
||||||
|
timeDiff,
|
||||||
|
|
||||||
|
/// Next message is by a different user
|
||||||
|
otherUser,
|
||||||
|
|
||||||
|
/// Message is deleted
|
||||||
|
deleted,
|
||||||
|
|
||||||
|
/// No other conditions are valid, default spacing (This will likely be the
|
||||||
|
/// only rule in the list provided)
|
||||||
|
defaultSpacing,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for building certain spacing after widgets.
|
||||||
|
/// This spacing can be in form of any widgets you like.
|
||||||
|
/// A List of [SpacingType] is provided to help inform the decision of
|
||||||
|
/// what to build after the message.
|
||||||
|
///
|
||||||
|
/// As an example:
|
||||||
|
/// MessageListView(
|
||||||
|
/// spacingWidgetBuilder: (context, list) {
|
||||||
|
/// if(list.contains(SpacingType.defaultSpacing)) {
|
||||||
|
/// return SizedBox(height: 2.0,);
|
||||||
|
/// } else {
|
||||||
|
/// return SizedBox(height: 8.0,);
|
||||||
|
/// }
|
||||||
|
/// },
|
||||||
|
/// ),
|
||||||
|
typedef SpacingWidgetBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
List<SpacingType> spacingTypes,
|
||||||
|
);
|
||||||
|
|
||||||
/// Class for message details
|
/// Class for message details
|
||||||
// ignore: prefer-match-file-name
|
// ignore: prefer-match-file-name
|
||||||
class MessageDetails {
|
class MessageDetails {
|
||||||
@@ -171,8 +211,14 @@ class MessageListView extends StatefulWidget {
|
|||||||
this.reverse = true,
|
this.reverse = true,
|
||||||
this.paginationLimit = 20,
|
this.paginationLimit = 20,
|
||||||
this.paginationLoadingIndicatorBuilder,
|
this.paginationLoadingIndicatorBuilder,
|
||||||
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
|
this.spacingWidgetBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
|
||||||
|
/// dismiss the keyboard automatically.
|
||||||
|
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
|
||||||
|
|
||||||
/// Function used to build a custom message widget
|
/// Function used to build a custom message widget
|
||||||
final MessageBuilder? messageBuilder;
|
final MessageBuilder? messageBuilder;
|
||||||
|
|
||||||
@@ -289,6 +335,12 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// Builder used to build the loading indicator shown while paginating.
|
/// Builder used to build the loading indicator shown while paginating.
|
||||||
final WidgetBuilder? paginationLoadingIndicatorBuilder;
|
final WidgetBuilder? paginationLoadingIndicatorBuilder;
|
||||||
|
|
||||||
|
/// This allows a user to customise the space after a message
|
||||||
|
/// A List of [SpacingType] is provided to provide more data about the
|
||||||
|
/// type of message (thread, difference in time between current and last
|
||||||
|
/// message, default spacing, etc)
|
||||||
|
final SpacingWidgetBuilder? spacingWidgetBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageListViewState createState() => _MessageListViewState();
|
_MessageListViewState createState() => _MessageListViewState();
|
||||||
}
|
}
|
||||||
@@ -443,9 +495,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
childAnchor: Alignment.topCenter,
|
childAnchor: Alignment.topCenter,
|
||||||
message: statusString,
|
message: statusString,
|
||||||
child: LazyLoadScrollView(
|
child: LazyLoadScrollView(
|
||||||
onPageScrollStart: () {
|
|
||||||
FocusScope.of(context).unfocus();
|
|
||||||
},
|
|
||||||
onStartOfPage: () async {
|
onStartOfPage: () async {
|
||||||
_inBetweenList = false;
|
_inBetweenList = false;
|
||||||
if (!_upToDate) {
|
if (!_upToDate) {
|
||||||
@@ -471,6 +520,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
key: (initialIndex != 0 && initialAlignment != 0)
|
key: (initialIndex != 0 && initialAlignment != 0)
|
||||||
? ValueKey('$initialIndex-$initialAlignment')
|
? ValueKey('$initialIndex-$initialAlignment')
|
||||||
: null,
|
: null,
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
itemPositionsListener: _itemPositionListener,
|
itemPositionsListener: _itemPositionListener,
|
||||||
initialScrollIndex: initialIndex,
|
initialScrollIndex: initialIndex,
|
||||||
initialAlignment: initialAlignment,
|
initialAlignment: initialAlignment,
|
||||||
@@ -564,17 +614,38 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
Units.MINUTE,
|
Units.MINUTE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final spacingRules = <SpacingType>[];
|
||||||
|
|
||||||
final isNextUserSame =
|
final isNextUserSame =
|
||||||
message.user!.id == nextMessage.user?.id;
|
message.user!.id == nextMessage.user?.id;
|
||||||
final isThread = message.replyCount! > 0;
|
final isThread = message.replyCount! > 0;
|
||||||
final isDeleted = message.isDeleted;
|
final isDeleted = message.isDeleted;
|
||||||
if (timeDiff >= 1 ||
|
final hasTimeDiff = timeDiff >= 1;
|
||||||
!isNextUserSame ||
|
|
||||||
isThread ||
|
if (hasTimeDiff) {
|
||||||
isDeleted) {
|
spacingRules.add(SpacingType.timeDiff);
|
||||||
return const SizedBox(height: 8);
|
|
||||||
}
|
}
|
||||||
return const SizedBox(height: 2);
|
|
||||||
|
if (!isNextUserSame) {
|
||||||
|
spacingRules.add(SpacingType.otherUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isThread) {
|
||||||
|
spacingRules.add(SpacingType.thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeleted) {
|
||||||
|
spacingRules.add(SpacingType.deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spacingRules.isNotEmpty) {
|
||||||
|
return widget.spacingWidgetBuilder
|
||||||
|
?.call(context, spacingRules) ??
|
||||||
|
const SizedBox(height: 8);
|
||||||
|
}
|
||||||
|
return widget.spacingWidgetBuilder
|
||||||
|
?.call(context, [SpacingType.defaultSpacing]) ??
|
||||||
|
const SizedBox(height: 2);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
if (i == itemCount - 1) {
|
if (i == itemCount - 1) {
|
||||||
|
|||||||
@@ -97,14 +97,20 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.deletedBottomRowBuilder,
|
this.deletedBottomRowBuilder,
|
||||||
this.onReturnAction,
|
this.onReturnAction,
|
||||||
this.customAttachmentBuilders,
|
this.customAttachmentBuilders,
|
||||||
this.readList,
|
|
||||||
this.padding,
|
this.padding,
|
||||||
this.textPadding = const EdgeInsets.symmetric(
|
this.textPadding = const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
this.attachmentPadding = EdgeInsets.zero,
|
this.attachmentPadding = EdgeInsets.zero,
|
||||||
this.allRead = false,
|
@Deprecated('''
|
||||||
|
allRead is now deprecated and it will be removed in future releases.
|
||||||
|
The MessageWidget now listens for read events on its own.
|
||||||
|
''') this.allRead = false,
|
||||||
|
@Deprecated('''
|
||||||
|
readList is now deprecated and it will be removed in future releases.
|
||||||
|
The MessageWidget now listens for read events on its own.
|
||||||
|
''') this.readList,
|
||||||
this.onQuotedMessageTap,
|
this.onQuotedMessageTap,
|
||||||
this.customActions = const [],
|
this.customActions = const [],
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
@@ -558,8 +564,6 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
|
|
||||||
bool get showTimeStamp => widget.showTimestamp;
|
bool get showTimeStamp => widget.showTimestamp;
|
||||||
|
|
||||||
bool get isMessageRead => widget.readList?.isNotEmpty == true;
|
|
||||||
|
|
||||||
bool get showInChannel => widget.showInChannelIndicator;
|
bool get showInChannel => widget.showInChannelIndicator;
|
||||||
|
|
||||||
bool get hasQuotedMessage => widget.message.quotedMessage != null;
|
bool get hasQuotedMessage => widget.message.quotedMessage != null;
|
||||||
@@ -1230,6 +1234,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
Widget _buildSendingIndicator() {
|
Widget _buildSendingIndicator() {
|
||||||
final style = widget.messageTheme.createdAtStyle;
|
final style = widget.messageTheme.createdAtStyle;
|
||||||
final message = widget.message;
|
final message = widget.message;
|
||||||
|
final memberCount = StreamChannel.of(context).channel.memberCount ?? 0;
|
||||||
|
|
||||||
if (hasNonUrlAttachments &&
|
if (hasNonUrlAttachments &&
|
||||||
(message.status == MessageSendingStatus.sending ||
|
(message.status == MessageSendingStatus.sending ||
|
||||||
@@ -1252,27 +1257,40 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget child = SendingIndicator(
|
final channel = StreamChannel.of(context).channel;
|
||||||
message: message,
|
|
||||||
isMessageRead: isMessageRead,
|
return BetterStreamBuilder<List<Read>>(
|
||||||
size: style!.fontSize,
|
stream: channel.state?.readStream,
|
||||||
|
initialData: channel.state?.read,
|
||||||
|
builder: (context, data) {
|
||||||
|
final readList = data.where((it) =>
|
||||||
|
it.user.id != _streamChat.currentUser?.id &&
|
||||||
|
(it.lastRead.isAfter(message.createdAt) ||
|
||||||
|
it.lastRead.isAtSameMomentAs(message.createdAt)));
|
||||||
|
final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||||
|
Widget child = SendingIndicator(
|
||||||
|
message: message,
|
||||||
|
isMessageRead: isMessageRead,
|
||||||
|
size: style!.fontSize,
|
||||||
|
);
|
||||||
|
if (isMessageRead) {
|
||||||
|
child = Row(
|
||||||
|
children: [
|
||||||
|
if (memberCount > 2)
|
||||||
|
Text(
|
||||||
|
readList.length.toString(),
|
||||||
|
style: style.copyWith(
|
||||||
|
color: _streamChatTheme.colorTheme.accentPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (isMessageRead) {
|
|
||||||
child = Row(
|
|
||||||
children: [
|
|
||||||
if (StreamChannel.of(context).channel.memberCount! > 2)
|
|
||||||
Text(
|
|
||||||
widget.readList!.length.toString(),
|
|
||||||
style: style.copyWith(
|
|
||||||
color: _streamChatTheme.colorTheme.accentPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 2),
|
|
||||||
child,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUserAvatar() => Transform.translate(
|
Widget _buildUserAvatar() => Transform.translate(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export 'package:jiffy/jiffy.dart';
|
|||||||
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
export 'src/attachment/attachment.dart';
|
export 'src/attachment/attachment.dart';
|
||||||
|
export 'src/attachment_actions_modal.dart';
|
||||||
export 'src/back_button.dart';
|
export 'src/back_button.dart';
|
||||||
export 'src/channel_avatar.dart';
|
export 'src/channel_avatar.dart';
|
||||||
export 'src/channel_header.dart';
|
export 'src/channel_header.dart';
|
||||||
|
|||||||
@@ -83,9 +83,12 @@ class HomeScreen extends StatelessWidget {
|
|||||||
channelListController: channelListController,
|
channelListController: channelListController,
|
||||||
filter: Filter.and([
|
filter: Filter.and([
|
||||||
Filter.equal('type', 'messaging'),
|
Filter.equal('type', 'messaging'),
|
||||||
Filter.in_('members', [
|
Filter.in_(
|
||||||
StreamChatCore.of(context).currentUser!.id,
|
'members',
|
||||||
])
|
[
|
||||||
|
StreamChatCore.of(context).currentUser!.id,
|
||||||
|
],
|
||||||
|
)
|
||||||
]),
|
]),
|
||||||
emptyBuilder: (BuildContext context) => const Center(
|
emptyBuilder: (BuildContext context) => const Center(
|
||||||
child: Text('Looks like you are not in any channels'),
|
child: Text('Looks like you are not in any channels'),
|
||||||
@@ -318,10 +321,10 @@ class _MessageScreenState extends State<MessageScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -227,13 +227,13 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
preferOffline: preferOffline,
|
preferOffline: preferOffline,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<List<ChannelState>> _queryAtMessage({
|
Future<ChannelState?> _queryAtMessage({
|
||||||
String? messageId,
|
String? messageId,
|
||||||
int before = 20,
|
int before = 20,
|
||||||
int after = 20,
|
int after = 20,
|
||||||
bool preferOffline = false,
|
bool preferOffline = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (channel.state == null) return [];
|
if (channel.state == null) return null;
|
||||||
channel.state!.isUpToDate = false;
|
channel.state!.isUpToDate = false;
|
||||||
channel.state!.truncate();
|
channel.state!.truncate();
|
||||||
|
|
||||||
@@ -245,23 +245,33 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
preferOffline: preferOffline,
|
preferOffline: preferOffline,
|
||||||
);
|
);
|
||||||
channel.state!.isUpToDate = true;
|
channel.state!.isUpToDate = true;
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Future.wait([
|
return queryAroundMessage(
|
||||||
queryBeforeMessage(
|
messageId,
|
||||||
messageId,
|
before: before,
|
||||||
limit: before,
|
after: after,
|
||||||
preferOffline: preferOffline,
|
preferOffline: preferOffline,
|
||||||
),
|
);
|
||||||
queryAfterMessage(
|
|
||||||
messageId,
|
|
||||||
limit: after,
|
|
||||||
preferOffline: preferOffline,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
Future<ChannelState> queryAroundMessage(
|
||||||
|
String messageId, {
|
||||||
|
int before = 20,
|
||||||
|
int after = 20,
|
||||||
|
bool preferOffline = false,
|
||||||
|
}) =>
|
||||||
|
channel.query(
|
||||||
|
messagesPagination: PaginationParams(
|
||||||
|
idAround: messageId,
|
||||||
|
before: before,
|
||||||
|
after: after,
|
||||||
|
),
|
||||||
|
preferOffline: preferOffline,
|
||||||
|
);
|
||||||
|
|
||||||
///
|
///
|
||||||
Future<ChannelState> queryBeforeMessage(
|
Future<ChannelState> queryBeforeMessage(
|
||||||
String messageId, {
|
String messageId, {
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ void main() {
|
|||||||
_stateSetter?.call(() => limit = 6);
|
_stateSetter?.call(() => limit = 6);
|
||||||
|
|
||||||
final updatedChannels = _generateChannels(mockClient, count: limit);
|
final updatedChannels = _generateChannels(mockClient, count: limit);
|
||||||
final updatedPagination = pagination.copyWith(limit: limit);
|
final updatedPagination = PaginationParams(limit: limit);
|
||||||
when(() => mockClient.queryChannels(
|
when(() => mockClient.queryChannels(
|
||||||
filter: any(named: 'filter'),
|
filter: any(named: 'filter'),
|
||||||
sort: any(named: 'sort'),
|
sort: any(named: 'sort'),
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ void main() {
|
|||||||
_stateSetter?.call(() => limit = 6);
|
_stateSetter?.call(() => limit = 6);
|
||||||
|
|
||||||
final updatedMessageResponseList = _generateMessages(count: limit);
|
final updatedMessageResponseList = _generateMessages(count: limit);
|
||||||
final updatedPagination = pagination.copyWith(limit: limit);
|
final updatedPagination = PaginationParams(limit: limit);
|
||||||
when(() => mockClient.search(
|
when(() => mockClient.search(
|
||||||
testFilter,
|
testFilter,
|
||||||
query: any(named: 'query'),
|
query: any(named: 'query'),
|
||||||
|
|||||||
@@ -189,9 +189,7 @@ void main() {
|
|||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
)).called(
|
)).called(1);
|
||||||
2, // Fetching After messages + Fetching Before messages,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -214,14 +212,10 @@ void main() {
|
|||||||
child: const Offstage(key: childKey),
|
child: const Offstage(key: childKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
final beforePagination = PaginationParams(
|
final paginationParams = PaginationParams(
|
||||||
lessThan: initialMessageId,
|
idAround: initialMessageId,
|
||||||
limit: 20,
|
after: 20,
|
||||||
);
|
before: 20,
|
||||||
|
|
||||||
final afterPagination = PaginationParams(
|
|
||||||
greaterThanOrEqual: initialMessageId,
|
|
||||||
limit: 20,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockChannel.initialized).thenAnswer((_) async => true);
|
when(() => mockChannel.initialized).thenAnswer((_) async => true);
|
||||||
@@ -232,17 +226,7 @@ void main() {
|
|||||||
state: any(named: 'state'),
|
state: any(named: 'state'),
|
||||||
watch: any(named: 'watch'),
|
watch: any(named: 'watch'),
|
||||||
presence: any(named: 'presence'),
|
presence: any(named: 'presence'),
|
||||||
messagesPagination: beforePagination,
|
messagesPagination: paginationParams,
|
||||||
membersPagination: any(named: 'membersPagination'),
|
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
|
||||||
preferOffline: any(named: 'preferOffline'),
|
|
||||||
)).thenAnswer((_) async => ChannelState(messages: messages));
|
|
||||||
|
|
||||||
when(() => mockChannel.query(
|
|
||||||
state: any(named: 'state'),
|
|
||||||
watch: any(named: 'watch'),
|
|
||||||
presence: any(named: 'presence'),
|
|
||||||
messagesPagination: afterPagination,
|
|
||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
@@ -267,17 +251,7 @@ void main() {
|
|||||||
state: any(named: 'state'),
|
state: any(named: 'state'),
|
||||||
watch: any(named: 'watch'),
|
watch: any(named: 'watch'),
|
||||||
presence: any(named: 'presence'),
|
presence: any(named: 'presence'),
|
||||||
messagesPagination: beforePagination,
|
messagesPagination: paginationParams,
|
||||||
membersPagination: any(named: 'membersPagination'),
|
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
|
||||||
preferOffline: any(named: 'preferOffline'),
|
|
||||||
)).called(1);
|
|
||||||
|
|
||||||
verify(() => mockChannel.query(
|
|
||||||
state: any(named: 'state'),
|
|
||||||
watch: any(named: 'watch'),
|
|
||||||
presence: any(named: 'presence'),
|
|
||||||
messagesPagination: afterPagination,
|
|
||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
@@ -285,29 +259,15 @@ void main() {
|
|||||||
|
|
||||||
_stateSetter?.call(() => initialMessageId = 'testInitialMessageId2');
|
_stateSetter?.call(() => initialMessageId = 'testInitialMessageId2');
|
||||||
|
|
||||||
final updatedBeforePagination = beforePagination.copyWith(
|
final updatedPaginationParams = paginationParams.copyWith(
|
||||||
lessThan: initialMessageId,
|
idAround: initialMessageId,
|
||||||
);
|
|
||||||
|
|
||||||
final updatedAfterPagination = afterPagination.copyWith(
|
|
||||||
greaterThanOrEqual: initialMessageId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockChannel.query(
|
when(() => mockChannel.query(
|
||||||
state: any(named: 'state'),
|
state: any(named: 'state'),
|
||||||
watch: any(named: 'watch'),
|
watch: any(named: 'watch'),
|
||||||
presence: any(named: 'presence'),
|
presence: any(named: 'presence'),
|
||||||
messagesPagination: updatedBeforePagination,
|
messagesPagination: updatedPaginationParams,
|
||||||
membersPagination: any(named: 'membersPagination'),
|
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
|
||||||
preferOffline: any(named: 'preferOffline'),
|
|
||||||
)).thenAnswer((_) async => ChannelState(messages: messages));
|
|
||||||
|
|
||||||
when(() => mockChannel.query(
|
|
||||||
state: any(named: 'state'),
|
|
||||||
watch: any(named: 'watch'),
|
|
||||||
presence: any(named: 'presence'),
|
|
||||||
messagesPagination: updatedAfterPagination,
|
|
||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
@@ -319,17 +279,7 @@ void main() {
|
|||||||
state: any(named: 'state'),
|
state: any(named: 'state'),
|
||||||
watch: any(named: 'watch'),
|
watch: any(named: 'watch'),
|
||||||
presence: any(named: 'presence'),
|
presence: any(named: 'presence'),
|
||||||
messagesPagination: updatedBeforePagination,
|
messagesPagination: updatedPaginationParams,
|
||||||
membersPagination: any(named: 'membersPagination'),
|
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
|
||||||
preferOffline: any(named: 'preferOffline'),
|
|
||||||
)).called(1);
|
|
||||||
|
|
||||||
verify(() => mockChannel.query(
|
|
||||||
state: any(named: 'state'),
|
|
||||||
watch: any(named: 'watch'),
|
|
||||||
presence: any(named: 'presence'),
|
|
||||||
messagesPagination: updatedAfterPagination,
|
|
||||||
membersPagination: any(named: 'membersPagination'),
|
membersPagination: any(named: 'membersPagination'),
|
||||||
watchersPagination: any(named: 'watchersPagination'),
|
watchersPagination: any(named: 'watchersPagination'),
|
||||||
preferOffline: any(named: 'preferOffline'),
|
preferOffline: any(named: 'preferOffline'),
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ void main() {
|
|||||||
_stateSetter?.call(() => limit = 6);
|
_stateSetter?.call(() => limit = 6);
|
||||||
|
|
||||||
final updatedUsers = _generateUsers(count: limit);
|
final updatedUsers = _generateUsers(count: limit);
|
||||||
final updatedPagination = pagination.copyWith(limit: limit);
|
final updatedPagination = PaginationParams(limit: limit);
|
||||||
when(() => mockClient.queryUsers(
|
when(() => mockClient.queryUsers(
|
||||||
filter: any(named: 'filter'),
|
filter: any(named: 'filter'),
|
||||||
sort: any(named: 'sort'),
|
sort: any(named: 'sort'),
|
||||||
|
|||||||
@@ -374,8 +374,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations {
|
|||||||
String get youText => 'You';
|
String get youText => 'You';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String galleryPaginationText(
|
String galleryPaginationText({
|
||||||
{required int currentPage, required int totalPages}) =>
|
required int currentPage,
|
||||||
|
required int totalPages,
|
||||||
|
}) =>
|
||||||
'$currentPage of $totalPages';
|
'$currentPage of $totalPages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ class _MessageViewState extends State<MessageView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ void main() {
|
|||||||
role: 'testRole',
|
role: 'testRole',
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
lastActive: DateTime.now(),
|
|
||||||
online: math.Random().nextBool(),
|
online: math.Random().nextBool(),
|
||||||
banned: math.Random().nextBool(),
|
banned: math.Random().nextBool(),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user