Merge remote-tracking branch 'origin/develop' into feat/localization

# Conflicts:
#	packages/stream_chat_flutter/example/lib/main.dart
#	packages/stream_chat_flutter/example/pubspec.yaml
#	packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart
#	packages/stream_chat_flutter/lib/src/channel_list_view.dart
#	packages/stream_chat_flutter/lib/src/gallery_footer.dart
#	packages/stream_chat_flutter/lib/src/message_input.dart
#	packages/stream_chat_flutter/lib/src/message_list_view.dart
#	packages/stream_chat_flutter/lib/src/user_item.dart
This commit is contained in:
xsahil03x
2021-07-19 12:49:07 +05:30
188 changed files with 6663 additions and 3145 deletions
+46 -11
View File
@@ -1,3 +1,49 @@
## 2.0.0
🛑️ Breaking Changes from `1.5.3`
- migrate this package to null safety
- `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor)
- `client.disconnect()` is now divided into two different functions
- `client.closeConnection()` -> for closing user websocket connection.
- `client.disconnectUser()` -> for disconnecting user and resetting client state.
- `client.devToken()` now returns a `Token` model instead of `String`.
- `ApiError` is removed in favor of `StreamChatError`
- `StreamChatError` -> parent type for all the stream errors.
- `StreamWebSocketError` -> for user websocket related errors.
- `StreamChatNetworkError` -> for network related errors.
- `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params
- `option.state` -> bool state
- `option.watch` -> bool watch
- `option.presence` -> bool presence
- `client.queryUsers()` options param is removed in favor of individual params
- `option.presence` -> bool presence
- Migrate this package to null safety
- Added typed filters
🐞 Fixed
- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet connection
- several minor fixes
- performance improvements
✅ Added
- New `Location` enum is introduced for easily changing the client location/baseUrl.
- New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection.
- New `client.partialUpdateMessage` and `channel.partialUpdateMessage` methods
- `connectWebSocket` parameter in connect user calls to use the client in "connection-less" mode.
🔄 Changed
- `baseURL` is now deprecated in favor of using `Location` to change data location.
## 2.0.0-nullsafety.8
🐞 Fixed
- Export `PushProvider` enum
## 2.0.0-nullsafety.7
🛑️ Breaking Changes from `2.0.0-nullsafety.6`
@@ -27,21 +73,10 @@
- `baseURL` is now deprecated in favor of using `Location` to change data location.
🐞 Fixed
- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet
connection
## 2.0.0-nullsafety.6
- Fix thread reply not working with attachments
- Minor fixes
## 2.0.0-nullsafety.6
- Fix thread reply not working with attachments
- Minor fixes
## 2.0.0-nullsafety.5
- Minor fixes
+76 -80
View File
@@ -13,7 +13,7 @@ Future<void> main() async {
await client.connectUser(
User(
id: 'cool-shadow-7',
extraData: {
extraData: const {
'image':
'https://getstream.io/random_png/?id=cool-shadow-7&amp;name=Cool+shadow',
},
@@ -57,12 +57,10 @@ class StreamExample extends StatelessWidget {
final Channel channel;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Stream Chat Dart Example',
home: HomeScreen(channel: channel),
);
}
Widget build(BuildContext context) => MaterialApp(
title: 'Stream Chat Dart Example',
home: HomeScreen(channel: channel),
);
}
/// Main screen of our application. The layout is comprised of an [AppBar]
@@ -167,83 +165,81 @@ class _MessageViewState extends State<MessageView> {
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: _messages.length,
reverse: true,
itemBuilder: (BuildContext context, int index) {
final item = _messages[index];
if (item.user?.id == widget.channel.client.uid) {
return Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(item.text ?? ''),
),
);
} else {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(item.text ?? ''),
),
);
}
},
Widget build(BuildContext context) => Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: _messages.length,
reverse: true,
itemBuilder: (BuildContext context, int index) {
final item = _messages[index];
if (item.user?.id == widget.channel.client.uid) {
return Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(item.text ?? ''),
),
);
} else {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(item.text ?? ''),
),
);
}
},
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Enter your message',
),
),
),
Material(
type: MaterialType.circle,
color: Colors.blue,
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () async {
// We can send a new message by calling `sendMessage` on
// the current channel. After sending a message, the
// TextField is cleared and the list view is scrolled
// to show the new item.
if (_controller.value.text.isNotEmpty) {
await widget.channel.sendMessage(
Message(text: _controller.value.text),
);
_controller.clear();
_updateList();
}
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: Icon(
Icons.send,
color: Colors.white,
),
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Enter your message',
),
),
),
)
],
),
)
],
);
}
Material(
type: MaterialType.circle,
color: Colors.blue,
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () async {
// We can send a new message by calling `sendMessage` on
// the current channel. After sending a message, the
// TextField is cleared and the list view is scrolled
// to show the new item.
if (_controller.value.text.isNotEmpty) {
await widget.channel.sendMessage(
Message(text: _controller.value.text),
);
_controller.clear();
_updateList();
}
},
child: const Padding(
padding: EdgeInsets.all(8),
child: Center(
child: Icon(
Icons.send,
color: Colors.white,
),
),
),
),
)
],
),
)
],
);
}
/// Helper extension for quickly retrieving
@@ -888,6 +888,7 @@ class Channel {
final oldMessage = state!.messages[oldIndex];
state!.updateChannelState(state!._channelState.copyWith(
messages: state?.messages?..remove(oldMessage),
channel: state?._channelState.channel,
));
} else {
// remove thread message if present
+56 -17
View File
@@ -220,27 +220,53 @@ class StreamChatClient {
/// Connects the current user, this triggers a connection to the API.
/// It returns a [Future] that resolves when the connection is setup.
Future<Event> connectUser(User user, String token) =>
_connectUser(user, token: Token.fromRawValue(token));
/// Pass [connectWebSocket]: false, if you want to connect to websocket
/// at a later stage or use the client in connection-less mode
Future<OwnUser> connectUser(
User user,
String token, {
bool connectWebSocket = true,
}) =>
_connectUser(
user,
token: Token.fromRawValue(token),
connectWebSocket: connectWebSocket,
);
/// Connects the current user using the [tokenProvider] to fetch the token.
/// It returns a [Future] that resolves when the connection is setup.
Future<Event> connectUserWithProvider(
User user, TokenProvider tokenProvider) =>
_connectUser(user, provider: tokenProvider);
Future<OwnUser> connectUserWithProvider(
User user,
TokenProvider tokenProvider, {
bool connectWebSocket = true,
}) =>
_connectUser(
user,
provider: tokenProvider,
connectWebSocket: connectWebSocket,
);
/// Connects the current user with an anonymous id, this triggers a connection
/// to the API. It returns a [Future] that resolves when the connection is
/// setup.
Future<Event> connectAnonymousUser() async {
Future<OwnUser> connectAnonymousUser({
bool connectWebSocket = true,
}) async {
final token = Token.anonymous();
final user = OwnUser(id: token.userId);
return _connectUser(user, token: token);
return _connectUser(
user,
token: token,
connectWebSocket: connectWebSocket,
);
}
/// Connects the current user as guest, this triggers a connection to the API.
/// It returns a [Future] that resolves when the connection is setup.
Future<Event> connectGuestUser(User user) async {
Future<OwnUser> connectGuestUser(
User user, {
bool connectWebSocket = true,
}) async {
final userId = user.id;
final anonymousToken = Token.anonymous(userId: userId);
@@ -253,13 +279,18 @@ class StreamChatClient {
_tokenManager.reset();
final guestUserToken = Token.fromRawValue(guestUser.accessToken);
return _connectUser(guestUser.user, token: guestUserToken);
return _connectUser(
guestUser.user,
token: guestUserToken,
connectWebSocket: connectWebSocket,
);
}
Future<Event> _connectUser(
Future<OwnUser> _connectUser(
User user, {
Token? token,
TokenProvider? provider,
bool connectWebSocket = true,
}) async {
if (_ws.connectionCompleter?.isCompleted == false) {
throw const StreamChatError(
@@ -268,7 +299,7 @@ class StreamChatClient {
);
}
logger.info('connecting user : ${user.id}');
logger.info('setting user : ${user.id}');
await _tokenManager.setTokenOrProvider(
user.id,
@@ -279,17 +310,21 @@ class StreamChatClient {
final ownUser = OwnUser.fromUser(user);
state.user = ownUser;
if (!connectWebSocket) {
return ownUser;
}
try {
if (_originalChatPersistenceClient != null) {
_chatPersistenceClient = _originalChatPersistenceClient;
await _chatPersistenceClient!.connect(ownUser.id);
}
final event = await openConnection();
return event;
final res = await openConnection();
return res;
} catch (e, stk) {
if (e is StreamWebSocketError && e.isRetriable) {
final event = await _chatPersistenceClient?.getConnectionInfo();
if (event != null) return event;
if (event != null) return event.me?.merge(ownUser) ?? ownUser;
}
logger.severe('error connecting user : ${ownUser.id}', e, stk);
rethrow;
@@ -297,7 +332,7 @@ class StreamChatClient {
}
/// Creates a new WebSocket connection with the current user.
Future<Event> openConnection() async {
Future<OwnUser> openConnection() async {
assert(
state.user != null,
'User is not set on client, '
@@ -327,7 +362,8 @@ class StreamChatClient {
_ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler);
try {
return await _ws.connect(user);
final event = await _ws.connect(user);
return event.me?.merge(user) ?? user;
} catch (e, stk) {
logger.severe('error connecting ws', e, stk);
rethrow;
@@ -390,7 +426,10 @@ class StreamChatClient {
await queryChannelsOnline(
filter: Filter.in_('cid', cids),
paginationParams: const PaginationParams(limit: 30),
).then((_) => sync(cids: cids, lastSyncAt: _lastSyncedAt));
);
if (persistenceEnabled) {
await sync(cids: cids, lastSyncAt: _lastSyncedAt);
}
}
handleEvent(Event(
type: EventType.connectionRecovered,
@@ -3,6 +3,7 @@ import 'package:stream_chat/src/core/models/device.dart';
import 'package:stream_chat/src/core/models/mute.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/core/util/serializer.dart';
import 'package:stream_chat/stream_chat.dart';
part 'own_user.g.dart';
@@ -25,6 +26,7 @@ class OwnUser extends User {
bool online = false,
Map<String, Object?> extraData = const {},
bool banned = false,
List<String> teams = const [],
}) : super(
id: id,
role: role,
@@ -34,6 +36,7 @@ class OwnUser extends User {
online: online,
extraData: extraData,
banned: banned,
teams: teams,
);
/// Create a new instance from a json
@@ -50,8 +53,69 @@ class OwnUser extends User {
online: user.online,
banned: user.banned,
extraData: user.extraData,
teams: user.teams,
);
/// Creates a copy of [OwnUser] with specified attributes overridden.
@override
OwnUser copyWith({
String? id,
String? role,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? lastActive,
bool? online,
Map<String, Object?>? extraData,
bool? banned,
List<String>? teams,
List<Mute>? channelMutes,
List<Device>? devices,
List<Mute>? mutes,
int? totalUnreadCount,
int? unreadChannels,
}) =>
OwnUser(
id: id ?? this.id,
banned: banned ?? this.banned,
role: role ?? this.role,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lastActive: lastActive ?? this.lastActive,
online: online ?? this.online,
extraData: extraData ?? this.extraData,
teams: teams ?? this.teams,
channelMutes: channelMutes ?? this.channelMutes,
devices: devices ?? this.devices,
mutes: mutes ?? this.mutes,
totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount,
unreadChannels: unreadChannels ?? this.unreadChannels,
);
/// Returns a new [OwnUser] that is a combination of this ownUser
/// and the given [other] ownUser.
OwnUser merge(OwnUser? other) {
if (other == null) {
return this;
}
return copyWith(
banned: other.banned,
channelMutes: other.channelMutes,
createdAt: other.createdAt,
devices: other.devices,
extraData: other.extraData,
id: other.id,
lastActive: other.lastActive,
mutes: other.mutes,
online: other.online,
role: other.role,
teams: other.teams,
totalUnreadCount: other.totalUnreadCount,
unreadChannels: other.unreadChannels,
updatedAt: other.updatedAt,
);
}
/// List of user devices
@JsonKey(includeIfNull: false, defaultValue: <Device>[])
final List<Device> devices;
@@ -47,9 +47,10 @@ class User extends Equatable {
/// User role
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
defaultValue: <String>[])
includeIfNull: false,
toJson: Serializer.readOnly,
defaultValue: <String>[],
)
final List<String> teams;
/// Date of user creation
@@ -13,6 +13,7 @@ export './src/core/api/attachment_file_uploader.dart'
export './src/core/api/requests.dart';
export './src/core/api/requests.dart';
export './src/core/api/responses.dart';
export './src/core/api/stream_chat_api.dart' show PushProvider;
export './src/core/error/error.dart';
export './src/core/models/action.dart';
export './src/core/models/attachment.dart';
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '2.0.0-nullsafety.7';
const PACKAGE_VERSION = '2.0.0';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 2.0.0-nullsafety.7
version: 2.0.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -555,7 +555,7 @@ void main() {
});
test(
'should directly update the state with message as deleted if the state is sending or failed',
'''should directly update the state with message as deleted if the state is sending or failed''',
() async {
const messageId = 'test-message-id';
final message = Message(
@@ -981,7 +981,7 @@ void main() {
);
test(
'should override previous reaction if present and `enforceUnique` is true',
'''should override previous reaction if present and `enforceUnique` is true''',
() async {
const userId = 'test-user-id';
const messageId = 'test-message-id';
@@ -1875,7 +1875,7 @@ void main() {
});
test(
'should send `typingStart` event if there is not already a typingEvent or the difference between the two is >= 2 seconds',
'''should send `typingStart` event if there is not already a typingEvent or the difference between the two is >= 2 seconds''',
() async {
final typingEvent = Event(type: EventType.typingStart);
@@ -47,12 +47,6 @@ void main() {
final user = User(id: 'test-user-id');
final token = Token.development(user.id).rawValue;
final event = Event(
type: EventType.healthCheck,
connectionId: 'fake-connection-id',
me: OwnUser.fromUser(user),
);
expectLater(
// skipping first seed status -> ConnectionStatus.disconnected
client.wsConnectionStatusStream.skip(1),
@@ -64,9 +58,7 @@ void main() {
final res = await client.connectUser(user, token);
expect(res, isNotNull);
expect(res.type, event.type);
expect(res.connectionId, event.connectionId);
expect(res.me, isSameUserAs(user));
expect(res, isSameUserAs(user));
});
test('`.connectUserWithProvider` should work fine', () async {
@@ -76,12 +68,6 @@ void main() {
return Token.development(userId).rawValue;
}
final event = Event(
type: EventType.healthCheck,
connectionId: 'fake-connection-id',
me: OwnUser.fromUser(user),
);
expectLater(
// skipping first seed status -> ConnectionStatus.disconnected
client.wsConnectionStatusStream.skip(1),
@@ -93,9 +79,7 @@ void main() {
final res = await client.connectUserWithProvider(user, tokenProvider);
expect(res, isNotNull);
expect(res.type, event.type);
expect(res.connectionId, event.connectionId);
expect(res.me, isSameUserAs(user));
expect(res, isSameUserAs(user));
});
group('`.connectGuestUser`', () {
@@ -110,12 +94,6 @@ void main() {
..accessToken = token,
);
final event = Event(
type: EventType.healthCheck,
connectionId: 'fake-connection-id',
me: OwnUser.fromUser(user),
);
expectLater(
// skipping first seed status -> ConnectionStatus.disconnected
client.wsConnectionStatusStream.skip(1),
@@ -127,9 +105,7 @@ void main() {
final res = await client.connectGuestUser(user);
expect(res, isNotNull);
expect(res.type, event.type);
expect(res.connectionId, event.connectionId);
expect(res.me, isSameUserAs(user));
expect(res, isSameUserAs(user));
verify(
() => api.guest.getGuestUser(any(that: isSameUserAs(user))),
@@ -175,9 +151,6 @@ void main() {
final res = await client.connectAnonymousUser();
expect(res, isNotNull);
expect(res.type, EventType.healthCheck);
expect(res.connectionId, 'fake-connection-id');
expect(res.me, isNotNull);
});
group('`.openConnection`', () {
@@ -330,6 +303,93 @@ void main() {
);
});
group('Connect user calls with `connectWebSocket`: false', () {
const apiKey = 'test-api-key';
late final api = FakeChatApi();
late StreamChatClient client;
setUpAll(() {
// fallback values
registerFallbackValue<User>(FakeUser());
});
setUp(() {
client = StreamChatClient(apiKey, chatApi: api);
});
tearDown(() {
client.dispose();
});
test('`.connectUser` should succeed without connecting', () async {
final user = User(id: 'test-user-id');
final token = Token.development(user.id).rawValue;
final res = await client.connectUser(
user,
token,
connectWebSocket: false,
);
expect(res, isSameUserAs(user));
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
});
test(
'`.connectUserWithProvider` should succeed without connecting',
() async {
final user = User(id: 'test-user-id');
Future<String> tokenProvider(String userId) async {
expect(userId, user.id);
return Token.development(userId).rawValue;
}
final res = await client.connectUserWithProvider(
user,
tokenProvider,
connectWebSocket: false,
);
expect(res, isSameUserAs(user));
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
},
);
test('`.connectGuestUser` should succeed without connecting', () async {
final user = User(id: 'test-user-id');
final token = Token.development(user.id).rawValue;
when(() => api.guest.getGuestUser(any(that: isSameUserAs(user))))
.thenAnswer(
(_) async => ConnectGuestUserResponse()
..user = user
..accessToken = token,
);
final res = await client.connectGuestUser(
user,
connectWebSocket: false,
);
expect(res, isSameUserAs(user));
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
verify(
() => api.guest.getGuestUser(any(that: isSameUserAs(user))),
).called(1);
});
test(
'`.connectAnonymousUser` should succeed without connecting',
() async {
final res = await client.connectAnonymousUser(
connectWebSocket: false,
);
expect(res, isNotNull);
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
},
);
});
group('Fake web-socket connection function with failure and persistence', () {
const apiKey = 'test-api-key';
late final api = FakeChatApi();
@@ -353,7 +413,7 @@ void main() {
});
test(
'`.connectUser` should connect successfully if persistence contains event',
'''`.connectUser` should connect successfully if persistence contains event''',
() async {
final user = User(id: 'test-user-id');
final token = Token.development(user.id).rawValue;
@@ -366,8 +426,7 @@ void main() {
final res = await client.connectUser(user, token);
expect(res, isNotNull);
expect(res.connectionId, 'test-connection-id');
expect(res.me?.id, user.id);
expect(res, isSameUserAs(user));
verify(persistence.getConnectionInfo).called(1);
verifyNoMoreInteractions(persistence);
@@ -375,7 +434,7 @@ void main() {
);
test(
'`.connectUserWithProvider` should connect successfully if persistence contains event',
'''`.connectUserWithProvider` should connect successfully if persistence contains event''',
() async {
final user = User(id: 'test-user-id');
Future<String> tokenProvider(String userId) async {
@@ -391,8 +450,7 @@ void main() {
final res = await client.connectUserWithProvider(user, tokenProvider);
expect(res, isNotNull);
expect(res.connectionId, 'test-connection-id');
expect(res.me?.id, user.id);
expect(res, isSameUserAs(user));
verify(persistence.getConnectionInfo).called(1);
verifyNoMoreInteractions(persistence);
@@ -400,7 +458,7 @@ void main() {
);
test(
'`.connectGuestUser` should connect successfully if persistence contains event',
'''`.connectGuestUser` should connect successfully if persistence contains event''',
() async {
final user = User(id: 'test-user-id');
final token = Token.development(user.id).rawValue;
@@ -420,8 +478,7 @@ void main() {
final res = await client.connectGuestUser(user);
expect(res, isNotNull);
expect(res.connectionId, 'test-connection-id');
expect(res.me?.id, user.id);
expect(res, isSameUserAs(user));
verify(persistence.getConnectionInfo).called(1);
verifyNoMoreInteractions(persistence);
@@ -432,7 +489,7 @@ void main() {
);
test(
'`.connectAnonymousUser` should connect successfully if persistence contains event',
'''`.connectAnonymousUser` should connect successfully if persistence contains event''',
() async {
final user = User(id: 'test-user-id');
@@ -446,8 +503,6 @@ void main() {
final res = await client.connectAnonymousUser();
expect(res, isNotNull);
expect(res.connectionId, 'test-connection-id');
expect(res.me?.id, user.id);
verify(persistence.getConnectionInfo).called(1);
verifyNoMoreInteractions(persistence);
@@ -488,7 +543,7 @@ void main() {
group('`.sync`', () {
test(
'should update persistence connectionInfo and lastSync when sync succeeds',
'''should update persistence connectionInfo and lastSync when sync succeeds''',
() async {
const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3'];
final lastSyncAt = DateTime.now();
@@ -662,7 +717,7 @@ void main() {
);
test(
'should never rethrow network call if persistence already emitted some channels',
'''should never rethrow network call if persistence already emitted some channels''',
() async {
final persistentChannelStates = List.generate(
3,
@@ -870,7 +925,7 @@ void main() {
});
test(
'should rethrow if `.queryChannelsOnline` throws and persistence channels are empty',
'''should rethrow if `.queryChannelsOnline` throws and persistence channels are empty''',
() async {
when(() => api.channel.queryChannels(
filter: any(named: 'filter'),
@@ -57,7 +57,7 @@ void main() {
});
test(
'connectionIdInterceptor should be added if connectionIdManager is provided',
'''connectionIdInterceptor should be added if connectionIdManager is provided''',
() {
const apiKey = 'api-key';
final client = StreamHttpClient(
@@ -45,7 +45,7 @@ void main() {
});
test(
'`setTokenOrProvider` should throw if both token and provider is not provided',
'''`setTokenOrProvider` should throw if both token and provider is not provided''',
() async {
expect(tokenManager.userId, isNull);
@@ -192,7 +192,7 @@ void main() {
test('custom with no operator', () {
const key = 'testKey';
const values = ['testValue'];
final filter = Filter.custom(key: key, value: values);
const filter = Filter.custom(key: key, value: values);
final encoded = json.encode(filter);
expect(
encoded,
@@ -41,5 +41,43 @@ void main() {
expect(ownUser.banned, user.banned);
expect(ownUser.extraData, user.extraData);
});
test('copyWith', () {
final user = OwnUser.fromJson(jsonFixture('own_user.json'));
var newUser = user.copyWith();
expect(newUser.id, user.id);
expect(newUser.role, user.role);
expect(newUser.name, user.name);
newUser = user.copyWith(
id: 'test',
role: 'test',
extraData: {
'name': 'test',
},
);
expect(newUser.id, 'test');
expect(newUser.role, 'test');
expect(newUser.name, 'test');
});
test('merge', () {
final user = OwnUser.fromJson(jsonFixture('own_user.json'));
final newUser = user.merge(OwnUser(
id: 'test',
role: 'test',
extraData: const {
'name': 'test',
},
banned: true,
));
expect(newUser.id, 'test');
expect(newUser.role, 'test');
expect(newUser.name, 'test');
expect(newUser.banned, true);
});
});
}
@@ -13,7 +13,7 @@ void main() {
expect(reaction.type, 'wow');
expect(
reaction.user?.toJson(),
User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: {
User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const {
'image': 'https://randomuser.me/api/portraits/women/45.jpg',
'name': 'Daisy Morgan'
}).toJson(),
@@ -28,7 +28,8 @@ void main() {
messageId: '76cd8c82-b557-4e48-9d12-87995d3a0e04',
createdAt: DateTime.parse('2020-01-28T22:17:31.108742Z'),
type: 'wow',
user: User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: {
user:
User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const {
'image': 'https://randomuser.me/api/portraits/women/45.jpg',
'name': 'Daisy Morgan'
}),
@@ -154,7 +154,6 @@ void main() {
});
test('updateChannelState', () async {
const cid = 'test:cid';
final channelState = ChannelState();
persistenceClient.updateChannelState(channelState);
});
+85
View File
@@ -1,3 +1,88 @@
## 2.0.0
🛑️ Breaking Changes from `1.5.4`
- Migrate this package to null safety
- Renamed `ChannelImage` to `ChannelAvatar`
- Updated `StreamChatThemeData.reactionIcons` to accept custom builder
- Renamed `ColorTheme` properties to reflect the purpose of the colors
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
- `ColorTheme.white` -> `ColorTheme.barsBg`
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
- `ChannelListCore` options property is removed in favor of individual properties
- `options.state` -> bool state
- `options.watch` -> bool watch
- `options.presence` -> bool presence
- `UserListView` options property is removed in favor of individual properties
- `options.presence` -> bool presence
- Renamed `ImageHeader` to `GalleryHeader`
- Renamed `ImageFooter` to `GalleryFooter`
- `MessageBuilder` and `ParentMessageBuilder` signature is now
```dart
typedef MessageBuilder = Widget Function(
BuildContext,
MessageDetails,
List<Message>,
MessageWidget defaultMessageWidget,
);
```
the last parameter is the default `MessageWidget`
You can call `.copyWith` to customize just a subset of properties
✅ Added
- Added video compress options (frame and quality) to `MessageInput`
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
- `MessageWidget` accepts a `userAvatarBuilder`
- Added pinMessage ui support
- Added `MessageListView.threadSeparatorBuilder` property
- Added `MessageInput.onError` property to allow error handling
- Added `GalleryHeader/GalleryFooter` theme classes
🐞 Fixed
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
- `MessageListView` not rendering if the user is not a member of the channel
- Fix `MessageInput` overflow when there are no actions
- Minor fixes and improvements
## 2.0.0-nullsafety.9
🛑️ Breaking Changes from `2.0.0-nullsafety.8`
- Renamed `ColorTheme` properties to reflect the purpose of the colors
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
- `ColorTheme.white` -> `ColorTheme.barsBg`
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
✅ Added
- Added video compress options (frame and quality) to `MessageInput`
## 2.0.0-nullsafety.8
🛑️ Breaking Changes from `2.0.0-nullsafety.7`
+1 -1
View File
@@ -28,7 +28,7 @@ It teaches you how to use this SDK and also shows how to make frequently require
## Example App
This repo includes a fully functional example app with setup instructions.
The example is available under the [example](https://github.com/GetStream/stream-chat-flutter/tree/master/example) folder.
The example is available under the [example](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_flutter/example) folder.
## Add dependency
Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter)
@@ -34,84 +34,96 @@ class _HindiStreamChatLocalizationsDelegate
void main() async {
WidgetsFlutterBinding.ensureInitialized();
/// Create a new instance of [StreamChatClient] passing the apikey obtained from your
/// project dashboard.
/// Create a new instance of [StreamChatClient] passing the apikey obtained
/// from your project dashboard.
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
/// Set the current user and connect the websocket. In a production scenario, this should be done using
/// a backend to generate a user token using our server SDK.
/// Set the current user and connect the websocket. In a production
/// scenario, this should be done using a backend to generate a user token
/// using our server SDK.
///
/// Please see the following for more information:
/// https://getstream.io/chat/docs/ios_user_setup_and_tokens/
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.'
'0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
);
final channel = client.channel('messaging', id: 'godevs');
await channel.watch();
runApp(MyApp(client, channel));
runApp(
MyApp(
client: client,
channel: channel,
),
);
}
/// Example application using Stream Chat Flutter widgets.
/// Stream Chat Flutter is a set of Flutter widgets which provide full chat functionalities
/// for building Flutter applications using Stream.
/// If you'd prefer using minimal wrapper widgets for your app, please see our other
///
/// Stream Chat Flutter is a set of Flutter widgets which provide full chat
/// functionalities for building Flutter applications using Stream. If you'd
/// prefer using minimal wrapper widgets for your app, please see our other
/// package, `stream_chat_flutter_core`.
class MyApp extends StatelessWidget {
/// Example using Stream's Flutter package.
///
/// If you'd prefer using minimal wrapper widgets for your app, please see
/// our other package, `stream_chat_flutter_core`.
const MyApp({
Key? key,
required this.client,
required this.channel,
}) : super(key: key);
/// Instance of Stream Client.
/// Stream's [StreamChatClient] can be used to connect to our servers and set the default
/// user for the application. Performing these actions trigger a websocket connection
/// allowing for real-time updates.
///
/// Stream's [StreamChatClient] can be used to connect to our servers and
/// set the default user for the application. Performing these actions
/// trigger a websocket connection allowing for real-time updates.
final StreamChatClient client;
/// Instance of the Channel
final Channel channel;
/// Example using Stream's Flutter package.
/// If you'd prefer using minimal wrapper widgets for your app, please see our other
/// package, `stream_chat_flutter_core`.
MyApp(this.client, this.channel);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
supportedLocales: [
Locale('en', 'US'),
Locale('hi', 'IN'),
],
localizationsDelegates: [
GlobalStreamChatLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
StreamChatLocalizationsHi.delegate,
],
builder: (context, widget) {
return StreamChat(
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
supportedLocales: [
Locale('en', 'US'),
Locale('hi', 'IN'),
],
localizationsDelegates: [
GlobalStreamChatLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
StreamChatLocalizationsHi.delegate,
],
builder: (context, widget) => StreamChat(
client: client,
child: widget,
);
},
home: StreamChannel(
channel: channel,
child: ChannelPage(),
),
);
}
),
home: StreamChannel(
channel: channel,
child: const ChannelPage(),
),
);
}
/// A list of messages sent in the current channel.
///
/// This is implemented using [MessageListView], a widget that provides query functionalities
/// fetching the messages from the api and showing them in a listView
/// This is implemented using [MessageListView], a widget that provides query
/// functionalities fetching the messages from the api and showing them in a
/// listView.
class ChannelPage extends StatelessWidget {
/// Creates the page that shows the list of messages
const ChannelPage({
@@ -119,17 +131,15 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
);
}
Widget build(BuildContext context) => Scaffold(
appBar: const ChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
);
}
@@ -10,30 +10,39 @@ void main() async {
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
runApp(MyApp(client));
runApp(
MyApp(
client: client,
),
);
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
required this.client,
}) : super(key: key);
final StreamChatClient client;
MyApp(this.client);
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: SplitView(),
);
}
Widget build(BuildContext context) => MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: const SplitView(),
);
}
class SplitView extends StatefulWidget {
const SplitView({
Key? key,
}) : super(key: key);
@override
_SplitViewState createState() => _SplitViewState();
}
@@ -42,69 +51,67 @@ class _SplitViewState extends State<SplitView> {
Channel? selectedChannel;
@override
Widget build(BuildContext context) {
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
flex: 1,
child: ChannelListPage(
onTap: (channel) {
setState(() {
selectedChannel = channel;
});
},
Widget build(BuildContext context) => Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
child: ChannelListPage(
onTap: (channel) {
setState(() {
selectedChannel = channel;
});
},
),
),
),
Flexible(
flex: 2,
child: Scaffold(
body: selectedChannel != null
? StreamChannel(
key: ValueKey(selectedChannel!.cid),
channel: selectedChannel!,
child: ChannelPage(),
)
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
Flexible(
flex: 2,
child: Scaffold(
body: selectedChannel != null
? StreamChannel(
key: ValueKey(selectedChannel!.cid),
channel: selectedChannel!,
child: const ChannelPage(),
)
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
),
),
),
),
),
),
],
);
}
],
);
}
class ChannelListPage extends StatelessWidget {
const ChannelListPage({
Key? key,
this.onTap,
}) : super(key: key);
final void Function(Channel)? onTap;
ChannelListPage({this.onTap});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
onChannelTap: onTap != null
? (channel, _) {
onTap!(channel);
}
: null,
filter: Filter.in_(
'members',
[StreamChat.of(context).user!.id],
),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
limit: 20,
Widget build(BuildContext context) => Scaffold(
body: ChannelsBloc(
child: ChannelListView(
onChannelTap: onTap != null
? (channel, _) {
onTap!(channel);
}
: null,
filter: Filter.in_(
'members',
[StreamChat.of(context).user!.id],
),
sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20,
),
),
),
),
);
}
);
}
class ChannelPage extends StatelessWidget {
@@ -113,27 +120,21 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Navigator(
onGenerateRoute: (settings) {
return MaterialPageRoute(
builder: (context) {
return Scaffold(
appBar: ChannelHeader(
showBackButton: false,
),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
);
},
);
},
);
}
Widget build(BuildContext context) => Navigator(
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => Scaffold(
appBar: const ChannelHeader(
showBackButton: false,
),
body: Column(
children: const <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
),
),
);
}
@@ -4,25 +4,30 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// First step of the [tutorial](https://getstream.io/chat/flutter/tutorial/)
///
/// There are three important things to notice that are common to all Flutter application using StreamChat:
/// There are three important things to notice that are common to all Flutter
/// application using StreamChat:
///
/// 1. The Dart API [StreamChatClient] is initialized with your API Key
/// 2. The current user is set by calling [StreamChatClient.connectUser]
/// 3. The client is then passed to the top-level [StreamChat] widget
/// [StreamChat] is an inherited widget and must be the parent of all Chat related widgets.
/// [StreamChat] is an inherited widget and must be the parent of all
/// Chat related widgets.
///
/// Please note that while Flutter can be used to build both mobile and web applications;
/// in this tutorial we focus on mobile, make sure when running the app you use a mobile device.
/// Please note that while Flutter can be used to build both mobile and web
/// applications, in this tutorial we focus on mobile. Make sure when running
/// the app that you use a mobile device.
///
/// Let's have a look at what we've built:
///
/// - We set up the Chat [StreamChatClient] with the API key
///
/// - We set the the current user for Chat with [StreamChatClient.connectUser] and a pre-generated user token
/// - We set the the current user for Chat with [StreamChatClient.connectUser]
/// and a pre-generated user token
///
/// - We make [StreamChat] the root Widget of our application
///
/// - We create a single [ChannelPage] widget under [StreamChat] with three widgets: [ChannelHeader], [MessageListView] and [MessageInput]
/// - We create a single [ChannelPage] widget under [StreamChat] with three
/// widgets: [ChannelHeader], [MessageListView] and [MessageInput]
///
/// If you now run the simulator you will see a single channel UI.
void main() async {
@@ -33,26 +38,38 @@ void main() async {
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
final channel = client.channel('messaging', id: 'godevs');
// ignore: unawaited_futures
// ignore: unawaited_futures, cascade_invocations
channel.watch();
runApp(MyApp(client, channel));
runApp(
MyApp(
client: client,
channel: channel,
),
);
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
required this.client,
required this.channel,
}) : super(key: key);
final StreamChatClient client;
final Channel channel;
MyApp(this.client, this.channel);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return MaterialApp(
// ignore: prefer_expression_function_bodies
builder: (context, widget) {
return StreamChat(
client: client,
@@ -61,7 +78,7 @@ class MyApp extends StatelessWidget {
},
home: StreamChannel(
channel: channel,
child: ChannelPage(),
child: const ChannelPage(),
),
);
}
@@ -73,11 +90,12 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
appBar: const ChannelHeader(),
body: Column(
children: <Widget>[
children: const <Widget>[
Expanded(
child: MessageListView(),
),
@@ -5,20 +5,29 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Second step of the [tutorial](https://getstream.io/chat/flutter/tutorial/)
///
/// Most chat applications handle more than just one single conversation.
/// Apps like Facebook Messenger, Whatsapp and Telegram allows you to have multiple one to one and group conversations.
/// Apps like Facebook Messenger, Whatsapp and Telegram allows you to have
/// multiple one-to-one and group conversations.
///
/// Let’s find out how we can change our application chat screen to display the list of conversations and navigate between them.
/// Let’s find out how we can change our application chat screen to display
/// the list of conversations and navigate between them.
///
/// > Note: the SDK uses Flutter’s [Navigator] to move from one route to another, this allows us to avoid any boiler-plate code.
/// > Of course you can take total control of how navigation works by customizing widgets like [Channel] and [ChannelList].
/// > Note: the SDK uses Flutter’s [Navigator] to move from one route to
/// another. This allows us to avoid any boiler-plate code.
/// > Of course, you can take total control of how navigation works by
/// customizing widgets like [Channel] and [ChannelList].
///
/// If you run the application, you will see that the first screen shows a list of conversations, you can open each by tapping and go back to the list.
/// If you run the application, you will see that the first screen shows a
/// list of conversations, you can open each by tapping and go back to the list.
///
/// Every single widget involved in this UI can be customized or swapped with your own.
/// Every single widget involved in this UI can be customized or swapped
/// with your own.
///
/// The [ChannelListPage] widget retrieves the list of channels based on a custom query and ordering.
/// In this case we are showing the list of channels the current user is a member and we order them based on the time they had a new message.
/// [ChannelListView] handles pagination and updates automatically out of the box when new channels are created or when a new message is added to a channel.
/// The [ChannelListPage] widget retrieves the list of channels based on a
/// custom query and ordering. In this case we are showing the list of
/// channels in which the current user is a member and we order them based
/// on the time they had a new message. [ChannelListView] handles pagination
/// and updates automatically when new channels are created or when a new
/// message is added to a channel.
void main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
@@ -27,31 +36,44 @@ void main() async {
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
runApp(MyApp(client));
runApp(
MyApp(
client: client,
),
);
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
required this.client,
}) : super(key: key);
final StreamChatClient client;
MyApp(this.client);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: ChannelListPage(),
home: const ChannelListPage(),
);
}
}
class ChannelListPage extends StatelessWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
@@ -60,11 +82,11 @@ class ChannelListPage extends StatelessWidget {
'members',
[StreamChat.of(context).user!.id],
),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20,
),
channelWidget: ChannelPage(),
channelWidget: const ChannelPage(),
),
),
);
@@ -77,11 +99,12 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
appBar: const ChannelHeader(),
body: Column(
children: <Widget>[
children: const <Widget>[
Expanded(
child: MessageListView(),
),
@@ -6,22 +6,30 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Third step of the [tutorial](https://getstream.io/chat/flutter/tutorial/)
///
/// So far you’ve learned how to use the default widgets.
/// The library has been designed with composition in mind and to allow all common customizations to be very easy.
/// This means that you can change any component in your application by swapping the default widgets with the ones you build yourself.
/// The library has been designed with composition in mind and to allow all
/// common customizations to be very easy.
/// This means that you can change any component in your application by
/// swapping the default widgets with the ones you build yourself.
///
/// Let’s see how we can make some changes to the SDK’s UI components.
/// We start by changing how channel previews are shown in the channel list and include the number of unread messages for each.
/// We start by changing how channel previews are shown in the channel list
/// and include the number of unread messages for each.
///
/// We're passing a custom widget to [ChannelListView.channelPreviewBuilder], this will override the default [ChannelPreview] and allows you to create one yourself.
/// We're passing a custom widget to [ChannelListView.channelPreviewBuilder];
/// this will override the default [ChannelPreview] and allows you to create
/// one yourself.
///
/// There are a couple interesting things we do in this widget:
///
/// - Instead of creating a whole new style for the channel name, we inherit the text style from the parent theme ([StreamChatTheme.of]) and only change the color attribute
/// - Instead of creating a whole new style for the channel name, we inherit
/// the text style from the parent theme ([StreamChatTheme.of]) and only
/// change the color attribute
///
/// - We loop over the list of channel messages to search for the first not deleted message ([Channel.state.messages])
/// - We loop over the list of channel messages to search for the first not
/// deleted message ([Channel.state.messages])
///
/// - We retrieve the count of unread messages from [Channel.state]
void main() async {
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -29,31 +37,44 @@ void main() async {
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
runApp(MyApp(client));
runApp(
MyApp(
client: client,
),
);
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
required this.client,
}) : super(key: key);
final StreamChatClient client;
MyApp(this.client);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: ChannelListPage(),
home: const ChannelListPage(),
);
}
}
class ChannelListPage extends StatelessWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
@@ -64,10 +85,10 @@ class ChannelListPage extends StatelessWidget {
),
channelPreviewBuilder: _channelPreviewBuilder,
// sort: [SortOption('last_message_at')],
pagination: PaginationParams(
pagination: const PaginationParams(
limit: 20,
),
channelWidget: ChannelPage(),
channelWidget: const ChannelPage(),
),
),
);
@@ -78,7 +99,7 @@ class ChannelListPage extends StatelessWidget {
(message) => !message.isDeleted,
);
final subtitle = (lastMessage == null ? 'nothing yet' : lastMessage.text!);
final subtitle = lastMessage == null ? 'nothing yet' : lastMessage.text!;
final opacity = (channel.state?.unreadCount ?? 0) > 0 ? 1.0 : 0.5;
return ListTile(
@@ -88,12 +109,12 @@ class ChannelListPage extends StatelessWidget {
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: ChannelPage(),
child: const ChannelPage(),
),
),
);
},
leading: ChannelImage(
leading: ChannelAvatar(
channel: channel,
),
title: ChannelName(
@@ -101,17 +122,17 @@ class ChannelListPage extends StatelessWidget {
StreamChatTheme.of(context).channelPreviewTheme.title!.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(opacity),
),
),
subtitle: Text(subtitle),
trailing: channel.state!.unreadCount! > 0
trailing: channel.state!.unreadCount > 0
? CircleAvatar(
radius: 10,
child: Text(channel.state!.unreadCount.toString()),
)
: SizedBox(),
: const SizedBox(),
);
}
}
@@ -122,11 +143,12 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
appBar: const ChannelHeader(),
body: Column(
children: <Widget>[
children: const <Widget>[
Expanded(
child: MessageListView(),
),
@@ -4,13 +4,17 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Fourth step of the [tutorial](https://getstream.io/chat/flutter/tutorial/)
///
/// Stream Chat supports message threads out of the box. Threads allows users to create sub-conversations inside the same channel.
/// Stream Chat supports message threads out of the box. Threads allows users
/// to create sub-conversations inside the same channel.
///
/// Using threaded conversations is very simple and mostly a matter of plugging the [MessageListView] to another widget that renders the widget.
/// To make this simple, such a widget only needs to build [MessageListView] with the parent attribute set to the thread’s root message.
/// Using threaded conversations is very simple and mostly a matter of
/// plugging the [MessageListView] to another widget that renders the widget.
/// To make this simple, such a widget only needs to build [MessageListView]
/// with the parent attribute set to the thread’s root message.
///
/// Now we can open threads and create new ones as well, if you long press a message you can tap on Reply and it will open the same [ThreadPage].
void main() async {
/// Now we can open threads and create new ones as well. If you long-press a
/// message, you can tap on "Reply" and it will open the same [ThreadPage].
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -18,33 +22,44 @@ void main() async {
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
runApp(MyApp(client));
runApp(
MyApp(
client: client,
),
);
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
required this.client,
}) : super(key: key);
final StreamChatClient client;
MyApp(this.client);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: Container(
child: ChannelListPage(),
),
home: const ChannelListPage(),
);
}
}
class ChannelListPage extends StatelessWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
@@ -53,11 +68,11 @@ class ChannelListPage extends StatelessWidget {
'members',
[StreamChat.of(context).user!.id],
),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20,
),
channelWidget: ChannelPage(),
channelWidget: const ChannelPage(),
),
),
);
@@ -70,21 +85,20 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
appBar: const ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
threadBuilder: (_, parentMessage) {
return ThreadPage(
parent: parentMessage,
);
},
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
),
),
MessageInput(),
const MessageInput(),
],
),
);
@@ -92,14 +106,15 @@ class ChannelPage extends StatelessWidget {
}
class ThreadPage extends StatelessWidget {
final Message? parent;
ThreadPage({
const ThreadPage({
Key? key,
this.parent,
}) : super(key: key);
final Message? parent;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ThreadHeader(
@@ -4,18 +4,23 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Fifth step of the [tutorial](https://getstream.io/chat/flutter/tutorial/)
///
/// Customizing how messages are rendered is another very common use-case that the SDK supports easily.
/// Customizing how messages are rendered is another very common use-case that
/// the SDK supports easily.
///
/// Replace the built-in message component with your own is done by passing it as a builder function to the [MessageListView] widget.
/// Replacing the built-in message component with your own is done by passing
/// it as a builder function to the [MessageListView] widget.
///
/// The message builder function will get the usual [BuildContext] argument as well as the [Message] object and its position inside the list.
/// The message builder function will get the usual [BuildContext] argument
/// as well as the [Message] object and its position inside the list.
///
/// If you look at the code you can see that we use [StreamChat.of] to retrieve the current user so that we can style messages own messages in a different way.
/// If you look at the code you can see that we use [StreamChat.of] to
/// retrieve the current user so that we can style messages in a different way.
///
/// Since custom widgets and builders are always children of [StreamChat] or part of a [Channel],
/// you can use [StreamChat.of], [StreamChannel.of] and [StreamChatTheme.of] to use the API client directly
/// or to retrieve outer scope needed such as messages from the [Channel.state].
void main() async {
/// Since custom widgets and builders are always children of [StreamChat] or
/// part of a [Channel], you can use [StreamChat.of], [StreamChannel.of],
/// and [StreamChatTheme.of] to use the API client directly or to retrieve
/// outer scope needed such as messages from the [Channel.state].
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -23,31 +28,44 @@ void main() async {
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
runApp(MyApp(client));
runApp(
MyApp(
client: client,
),
);
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
required this.client,
}) : super(key: key);
final StreamChatClient client;
MyApp(this.client);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: ChannelListPage(),
home: const ChannelListPage(),
);
}
}
class ChannelListPage extends StatelessWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
@@ -56,11 +74,11 @@ class ChannelListPage extends StatelessWidget {
'members',
[StreamChat.of(context).user!.id],
),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20,
),
channelWidget: ChannelPage(),
channelWidget: const ChannelPage(),
),
),
);
@@ -73,9 +91,10 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
appBar: const ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
@@ -83,7 +102,7 @@ class ChannelPage extends StatelessWidget {
messageBuilder: _messageBuilder,
),
),
MessageInput(),
const MessageInput(),
],
),
);
@@ -101,12 +120,14 @@ class ChannelPage extends StatelessWidget {
final color = isCurrentUser ? Colors.blueGrey : Colors.blue;
return Padding(
padding: EdgeInsets.all(5.0),
padding: const EdgeInsets.all(5),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: color, width: 1),
borderRadius: BorderRadius.all(
Radius.circular(5.0),
border: Border.all(
color: color,
),
borderRadius: const BorderRadius.all(
Radius.circular(5),
),
),
child: ListTile(
@@ -115,7 +136,7 @@ class ChannelPage extends StatelessWidget {
textAlign: textAlign,
),
subtitle: Text(
message.user!.extraData['name'] as String,
message.user!.name,
textAlign: textAlign,
),
),
@@ -4,22 +4,29 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Sixth step of the [tutorial](https://getstream.io/chat/flutter/tutorial/)
///
/// The Flutter SDK comes with a fully designed set of widgets which you can customize to fit with your application style and typography.
/// Changing the theme of Chat widgets works in a very similar way that [MaterialApp] and [Theme] do.
/// The Flutter SDK comes with a fully designed set of widgets which you can
/// customize to fit with your application style and typography.
/// Changing the theme of Chat widgets works in a very similar way that
/// [MaterialApp] and [Theme] do.
///
/// Out of the box all chat widgets use their own default styling, there are two ways to change the styling:
/// All chat widgets use their own default styling out of the box. There are
/// two ways to change the styling:
///
/// 1. Initialize the [StreamChatTheme] from your existing [MaterialApp] style
/// 2. Construct a custom theme and provide all the customizations needed
///
/// First we create a new Material [Theme] and pick [Colors.green] as swatch color. The theme is then passed to [MaterialApp] as usual.
/// First, we create a new Material [Theme] and pick [Colors.green] as the
/// swatch color. The theme is then passed to [MaterialApp] as usual.
///
/// Then we create a new [StreamChatTheme] from the green theme we just created.
/// After saving the app you will see the UI will update several widgets to match with the new color.
/// Then, we create a new [StreamChatTheme] from the green theme we just
/// created. After saving the app you will see that several widgets have
/// been updated with the new color.
///
/// We also change the message color posted by the current user.
/// You can perform these more granular style changes using [StreamChatTheme.copyWith].
void main() async {
///
/// You can perform these more granular style changes using
/// [StreamChatTheme.copyWith].
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -27,16 +34,23 @@ void main() async {
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'''eyJ0eXAiO«iJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
runApp(MyApp(client));
runApp(
MyApp(
client: client,
),
);
}
class MyApp extends StatelessWidget {
final StreamChatClient client;
const MyApp({
Key? key,
required this.client,
}) : super(key: key);
MyApp(this.client);
final StreamChatClient client;
@override
Widget build(BuildContext context) {
@@ -50,9 +64,9 @@ class MyApp extends StatelessWidget {
),
),
otherMessageTheme: MessageTheme(
messageBackgroundColor: colorTheme.black,
messageBackgroundColor: colorTheme.textHighEmphasis,
messageText: TextStyle(
color: colorTheme.white,
color: colorTheme.barsBg,
),
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(8),
@@ -62,20 +76,23 @@ class MyApp extends StatelessWidget {
return MaterialApp(
theme: themeData,
builder: (context, child) {
return StreamChat(
client: client,
streamChatThemeData: customTheme,
child: child,
);
},
home: ChannelListPage(),
builder: (context, child) => StreamChat(
client: client,
streamChatThemeData: customTheme,
child: child,
),
home: const ChannelListPage(),
);
}
}
class ChannelListPage extends StatelessWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
@@ -84,11 +101,11 @@ class ChannelListPage extends StatelessWidget {
'members',
[StreamChat.of(context).user!.id],
),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20,
),
channelWidget: ChannelPage(),
channelWidget: const ChannelPage(),
),
),
);
@@ -101,21 +118,20 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
appBar: const ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
threadBuilder: (_, parentMessage) {
return ThreadPage(
parent: parentMessage,
);
},
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
),
),
MessageInput(),
const MessageInput(),
],
),
);
@@ -123,14 +139,15 @@ class ChannelPage extends StatelessWidget {
}
class ThreadPage extends StatelessWidget {
final Message? parent;
ThreadPage({
const ThreadPage({
Key? key,
this.parent,
}) : super(key: key);
final Message? parent;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ThreadHeader(
@@ -21,23 +21,23 @@ environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
collection: ^1.15.0
cupertino_icons: ^1.0.3
flutter:
sdk: flutter
# stream_chat:
# path: ../../stream_chat
# stream_chat_flutter_core:
# path: ../../stream_chat_flutter_core
# stream_chat:
# path: ../../stream_chat
# stream_chat_flutter_core:
# path: ../../stream_chat_flutter_core
stream_chat_flutter:
path: ../
stream_chat_localizations:
path: ../../stream_chat_localizations
stream_chat_persistence:
stream_chat_persistence:
path: ../../stream_chat_persistence
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
collection: ^1.15.0
dev_dependencies:
flutter_test:
@@ -35,7 +35,7 @@ class AttachmentTitle extends StatelessWidget {
attachment.title!,
overflow: TextOverflow.ellipsis,
style: messageTheme.messageText?.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
fontWeight: FontWeight.bold,
),
),
@@ -130,7 +130,7 @@ class _PreparingState extends StatelessWidget {
alignment: Alignment.topRight,
child: _IconButton(
icon: StreamSvgIcon.close(
color: StreamChatTheme.of(context).colorTheme.white,
color: StreamChatTheme.of(context).colorTheme.barsBg,
),
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
),
@@ -170,7 +170,7 @@ class _InProgressState extends StatelessWidget {
alignment: Alignment.topRight,
child: _IconButton(
icon: StreamSvgIcon.close(
color: StreamChatTheme.of(context).colorTheme.white,
color: StreamChatTheme.of(context).colorTheme.barsBg,
),
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
),
@@ -209,7 +209,7 @@ class _FailedState extends StatelessWidget {
children: [
_IconButton(
icon: StreamSvgIcon.retry(
color: theme.colorTheme.white,
color: theme.colorTheme.barsBg,
),
onPressed: () {
channel.retryAttachmentUpload(messageId, attachmentId);
@@ -229,7 +229,7 @@ class _FailedState extends StatelessWidget {
child: Text(
context.translations.uploadErrorLabel,
style: theme.textTheme.footnote.copyWith(
color: theme.colorTheme.white,
color: theme.colorTheme.barsBg,
),
),
),
@@ -248,7 +248,7 @@ class _SuccessState extends StatelessWidget {
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
maxRadius: 12,
child: StreamSvgIcon.check(
color: StreamChatTheme.of(context).colorTheme.white,
color: StreamChatTheme.of(context).colorTheme.barsBg,
),
),
);
@@ -75,12 +75,14 @@ class AttachmentError extends StatelessWidget {
child: Container(
width: size?.width,
height: size?.height,
color:
StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
color: StreamChatTheme.of(context)
.colorTheme
.accentError
.withOpacity(.1),
child: Center(
child: Icon(
Icons.error_outline,
color: StreamChatTheme.of(context).colorTheme.black,
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
),
),
),
@@ -54,10 +54,10 @@ class FileAttachment extends AttachmentWidget {
width: size?.width ?? 100,
height: 56,
decoration: BoxDecoration(
color: colorTheme.white,
color: colorTheme.barsBg,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: colorTheme.greyWhisper,
color: colorTheme.borders,
),
),
child: Row(
@@ -141,8 +141,8 @@ class FileAttachment extends AttachmentWidget {
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Shimmer.fromColors(
baseColor: colorTheme.greyGainsboro,
highlightColor: colorTheme.whiteSmoke,
baseColor: colorTheme.disabled,
highlightColor: colorTheme.inputBg,
child: image,
);
},
@@ -217,7 +217,7 @@ class FileAttachment extends AttachmentWidget {
preparing: () => Padding(
padding: const EdgeInsets.all(8),
child: _buildButton(
icon: StreamSvgIcon.close(color: theme.colorTheme.white),
icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg),
fillColor: theme.colorTheme.overlayDark,
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
),
@@ -225,7 +225,7 @@ class FileAttachment extends AttachmentWidget {
inProgress: (_, __) => Padding(
padding: const EdgeInsets.all(8),
child: _buildButton(
icon: StreamSvgIcon.close(color: theme.colorTheme.white),
icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg),
fillColor: theme.colorTheme.overlayDark,
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
),
@@ -233,15 +233,15 @@ class FileAttachment extends AttachmentWidget {
success: () => Padding(
padding: const EdgeInsets.all(8),
child: CircleAvatar(
backgroundColor: theme.colorTheme.accentBlue,
backgroundColor: theme.colorTheme.accentPrimary,
maxRadius: 12,
child: StreamSvgIcon.check(color: theme.colorTheme.white),
child: StreamSvgIcon.check(color: theme.colorTheme.barsBg),
),
),
failed: (_) => Padding(
padding: const EdgeInsets.all(8),
child: _buildButton(
icon: StreamSvgIcon.retry(color: theme.colorTheme.white),
icon: StreamSvgIcon.retry(color: theme.colorTheme.barsBg),
fillColor: theme.colorTheme.overlayDark,
onPressed: () => channel.retryAttachmentUpload(
message.id,
@@ -253,7 +253,8 @@ class FileAttachment extends AttachmentWidget {
if (message.status == MessageSendingStatus.sent) {
trailingWidget = IconButton(
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
icon: StreamSvgIcon.cloudDownload(
color: theme.colorTheme.textHighEmphasis),
visualDensity: VisualDensity.compact,
splashRadius: 16,
onPressed: () {
@@ -272,7 +273,7 @@ class FileAttachment extends AttachmentWidget {
final theme = StreamChatTheme.of(context);
final size = attachment.file?.size ?? attachment.extraData['file_size'];
final textStyle = theme.textTheme.footnote.copyWith(
color: theme.colorTheme.grey,
color: theme.colorTheme.textLowEmphasis,
);
return attachment.uploadState.when(
preparing: () => Text(fileSize(size), style: textStyle),
@@ -282,7 +283,7 @@ class FileAttachment extends AttachmentWidget {
showBackground: false,
padding: EdgeInsets.zero,
textStyle: textStyle,
progressIndicatorColor: theme.colorTheme.accentBlue,
progressIndicatorColor: theme.colorTheme.accentPrimary,
),
success: () => Text(fileSize(size), style: textStyle),
failed: (_) => Text(
@@ -52,7 +52,7 @@ class GiphyAttachment extends AttachmentWidget {
mainAxisSize: MainAxisSize.min,
children: [
Card(
color: StreamChatTheme.of(context).colorTheme.white,
color: StreamChatTheme.of(context).colorTheme.barsBg,
elevation: 2,
clipBehavior: Clip.hardEdge,
shape: const RoundedRectangleBorder(
@@ -84,7 +84,7 @@ class GiphyAttachment extends AttachmentWidget {
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(0.5),
),
overflow: TextOverflow.ellipsis,
@@ -118,7 +118,7 @@ class GiphyAttachment extends AttachmentWidget {
Container(
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(0.2),
width: double.infinity,
height: 0.5,
@@ -142,7 +142,7 @@ class GiphyAttachment extends AttachmentWidget {
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(0.5),
),
),
@@ -153,7 +153,7 @@ class GiphyAttachment extends AttachmentWidget {
width: 0.5,
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(0.2),
height: 50,
),
@@ -174,7 +174,7 @@ class GiphyAttachment extends AttachmentWidget {
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(0.5),
),
maxLines: 1,
@@ -186,7 +186,7 @@ class GiphyAttachment extends AttachmentWidget {
width: 0.5,
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(0.2),
height: 50,
),
@@ -204,7 +204,7 @@ class GiphyAttachment extends AttachmentWidget {
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
.accentPrimary,
fontWeight: FontWeight.bold,
),
),
@@ -227,7 +227,7 @@ class GiphyAttachment extends AttachmentWidget {
StreamSvgIcon.eye(
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(0.5),
size: 16,
),
@@ -242,7 +242,7 @@ class GiphyAttachment extends AttachmentWidget {
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(0.5)),
),
],
@@ -307,8 +307,8 @@ class GiphyAttachment extends AttachmentWidget {
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Shimmer.fromColors(
baseColor: colorTheme.greyGainsboro,
highlightColor: colorTheme.whiteSmoke,
baseColor: colorTheme.disabled,
highlightColor: colorTheme.inputBg,
child: image,
);
},
@@ -323,7 +323,7 @@ class GiphyAttachment extends AttachmentWidget {
child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.black
.textHighEmphasis
.withOpacity(.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@@ -336,13 +336,14 @@ class GiphyAttachment extends AttachmentWidget {
child: Row(
children: [
StreamSvgIcon.lightning(
color: StreamChatTheme.of(context).colorTheme.white,
color: StreamChatTheme.of(context).colorTheme.barsBg,
size: 16,
),
Text(
context.translations.giphyLabel.toUpperCase(),
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.white,
color:
StreamChatTheme.of(context).colorTheme.barsBg,
fontWeight: FontWeight.bold,
fontSize: 11,
),
@@ -103,8 +103,8 @@ class ImageAttachment extends AttachmentWidget {
);
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Shimmer.fromColors(
baseColor: colorTheme.greyGainsboro,
highlightColor: colorTheme.whiteSmoke,
baseColor: colorTheme.disabled,
highlightColor: colorTheme.inputBg,
child: image,
);
},
@@ -72,7 +72,7 @@ class AttachmentActionsModal extends StatelessWidget {
context.translations.replyLabel,
StreamSvgIcon.iconCurveLineLeftUp(
size: 24,
color: theme.colorTheme.grey,
color: theme.colorTheme.textLowEmphasis,
),
() {
Navigator.pop(context, ReturnActionType.reply);
@@ -83,7 +83,7 @@ class AttachmentActionsModal extends StatelessWidget {
context.translations.showInChatLabel,
StreamSvgIcon.eye(
size: 24,
color: theme.colorTheme.black,
color: theme.colorTheme.textHighEmphasis,
),
onShowMessage,
),
@@ -94,7 +94,7 @@ class AttachmentActionsModal extends StatelessWidget {
: context.translations.saveImageLabel,
StreamSvgIcon.iconSave(
size: 24,
color: theme.colorTheme.grey,
color: theme.colorTheme.textLowEmphasis,
),
() {
final attachment = message.attachments[currentIndex];
@@ -145,7 +145,7 @@ class AttachmentActionsModal extends StatelessWidget {
context.translations.deleteLabel,
StreamSvgIcon.delete(
size: 24,
color: theme.colorTheme.accentRed,
color: theme.colorTheme.accentError,
),
() {
final channel = StreamChannel.of(context).channel;
@@ -166,7 +166,7 @@ class AttachmentActionsModal extends StatelessWidget {
..maybePop();
}
},
color: theme.colorTheme.accentRed,
color: theme.colorTheme.accentError,
),
]
.map<Widget>((e) => Align(
@@ -176,7 +176,7 @@ class AttachmentActionsModal extends StatelessWidget {
.insertBetween(
Container(
height: 1,
color: theme.colorTheme.greyWhisper,
color: theme.colorTheme.borders,
),
),
),
@@ -197,7 +197,7 @@ class AttachmentActionsModal extends StatelessWidget {
}) =>
Material(
key: key,
color: StreamChatTheme.of(context).colorTheme.white,
color: StreamChatTheme.of(context).colorTheme.barsBg,
child: InkWell(
onTap: onTap,
child: Padding(
@@ -242,7 +242,7 @@ class AttachmentActionsModal extends StatelessWidget {
width: 182,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: theme.colorTheme.white,
color: theme.colorTheme.barsBg,
),
child: Center(
child: progress == null
@@ -250,7 +250,7 @@ class AttachmentActionsModal extends StatelessWidget {
height: 100,
width: 100,
child: StreamSvgIcon.error(
color: theme.colorTheme.greyGainsboro,
color: theme.colorTheme.disabled,
),
)
: progress.toProgressIndicatorValue == 1.0
@@ -259,7 +259,7 @@ class AttachmentActionsModal extends StatelessWidget {
height: 160,
width: 160,
child: StreamSvgIcon.check(
color: theme.colorTheme.greyGainsboro,
color: theme.colorTheme.disabled,
),
)
: SizedBox(
@@ -272,14 +272,14 @@ class AttachmentActionsModal extends StatelessWidget {
value: progress.toProgressIndicatorValue,
strokeWidth: 8,
valueColor: AlwaysStoppedAnimation(
theme.colorTheme.accentBlue,
theme.colorTheme.accentPrimary,
),
),
Center(
child: Text(
'${progress.toPercentage}%',
style: theme.textTheme.headline.copyWith(
color: theme.colorTheme.grey,
color: theme.colorTheme.textLowEmphasis,
),
),
),
@@ -44,7 +44,7 @@ class StreamBackButton extends StatelessWidget {
padding: const EdgeInsets.all(14),
child: StreamSvgIcon.left(
size: 24,
color: StreamChatTheme.of(context).colorTheme.black,
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
),
),
if (showUnreads)
@@ -0,0 +1,202 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/group_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image_paint.png)
///
/// It shows the current [Channel] image.
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final StreamChatClient client;
/// final Channel channel;
///
/// MyApp(this.client, this.channel);
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// debugShowCheckedModeBanner: false,
/// home: StreamChat(
/// client: client,
/// child: StreamChannel(
/// channel: channel,
/// child: Center(
/// child: ChannelImage(
/// channel: channel,
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// The widget uses a [StreamBuilder] to render the channel information
/// image as soon as it updates.
///
/// By default the widget radius size is 40x40 pixels.
/// Set the property [constraints] to set a custom dimension.
///
/// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance.
class ChannelAvatar extends StatelessWidget {
/// Instantiate a new ChannelImage
const ChannelAvatar({
Key? key,
this.channel,
this.constraints,
this.onTap,
this.borderRadius,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
}) : super(key: key);
/// [BorderRadius] to display the widget
final BorderRadius? borderRadius;
/// The channel to show the image of
final Channel? channel;
/// The diameter of the image
final BoxConstraints? constraints;
/// The function called when the image is tapped
final VoidCallback? onTap;
/// If image is selected
final bool selected;
/// Selection color for image
final Color? selectionColor;
/// Thickness of selection image
final double selectionThickness;
@override
Widget build(BuildContext context) {
final streamChat = StreamChat.of(context);
final channel = this.channel ?? StreamChannel.of(context).channel;
assert(channel.state != null, 'Channel ${channel.id} is not initialized');
final chatThemeData = StreamChatTheme.of(context);
final colorTheme = chatThemeData.colorTheme;
final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme;
return BetterStreamBuilder<Map<String, dynamic>>(
stream: channel.extraDataStream,
initialData: channel.extraData,
builder: (context, extraData) {
final channelImage = extraData['image'];
if (channelImage != null) {
Widget child = ClipRRect(
borderRadius: borderRadius ?? previewTheme?.borderRadius,
child: Container(
constraints: constraints ?? previewTheme?.constraints,
decoration: BoxDecoration(color: colorTheme.accentPrimary),
child: InkWell(
onTap: onTap,
child: CachedNetworkImage(
imageUrl: channelImage,
errorWidget: (_, __, ___) => Center(
child: Text(
extraData['name']?[0] ?? '',
style: TextStyle(
color: colorTheme.barsBg,
fontWeight: FontWeight.bold,
),
),
),
fit: BoxFit.cover,
),
),
),
);
if (selected) {
child = ClipRRect(
key: const Key('selectedImage'),
borderRadius: BorderRadius.circular(selectionThickness) +
(borderRadius ??
previewTheme?.borderRadius ??
BorderRadius.zero),
child: Container(
constraints: constraints ?? previewTheme?.constraints,
color: selectionColor ?? colorTheme.accentPrimary,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: child,
),
),
);
}
return child;
}
final currentUser = streamChat.user!;
final otherMembers = channel.state!.members
.where((it) => it.userId != currentUser.id)
.toList(growable: false);
// our own space, no other members
if (otherMembers.isEmpty) {
return BetterStreamBuilder<User>(
stream: streamChat.client.state.userStream.map((it) => it!),
initialData: currentUser,
builder: (context, user) => UserAvatar(
borderRadius: borderRadius ?? previewTheme?.borderRadius,
user: user,
constraints: constraints ?? previewTheme?.constraints,
onTap: onTap != null ? (_) => onTap!() : null,
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary,
selectionThickness: selectionThickness,
),
);
}
// 1-1 Conversation
if (otherMembers.length == 1) {
final member = otherMembers.first;
return BetterStreamBuilder<Member>(
stream: channel.state!.membersStream.map(
(members) => members.firstWhere(
(it) => it.userId == member.userId,
orElse: () => member,
),
),
initialData: member,
builder: (context, member) => UserAvatar(
borderRadius: borderRadius ?? previewTheme?.borderRadius,
user: member.user!,
constraints: constraints ?? previewTheme?.constraints,
onTap: onTap != null ? (_) => onTap!() : null,
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary,
selectionThickness: selectionThickness,
),
);
}
// Group conversation
return GroupAvatar(
members: otherMembers,
borderRadius: borderRadius ?? previewTheme?.borderRadius,
constraints: constraints ?? previewTheme?.constraints,
onTap: onTap,
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary,
selectionThickness: selectionThickness,
);
},
);
}
}
@@ -33,7 +33,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
final isOwner = userAsMember.role == 'owner';
return Material(
color: _streamChatThemeData.colorTheme.white,
color: _streamChatThemeData.colorTheme.barsBg,
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
@@ -147,7 +147,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.user(
color: _streamChatThemeData.colorTheme.grey,
color: _streamChatThemeData.colorTheme.textLowEmphasis,
),
),
title: context.translations.viewInfoLabel,
@@ -158,7 +158,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.userRemove(
color: _streamChatThemeData.colorTheme.grey,
color: _streamChatThemeData.colorTheme.textLowEmphasis,
),
),
title: context.translations.leaveGroupLabel,
@@ -177,11 +177,11 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.delete(
color: _streamChatThemeData.colorTheme.accentRed,
color: _streamChatThemeData.colorTheme.accentError,
),
),
title: context.translations.deleteConversationLabel,
titleColor: _streamChatThemeData.colorTheme.accentRed,
titleColor: _streamChatThemeData.colorTheme.accentError,
onTap: () async {
setState(() {
_showActions = false;
@@ -196,7 +196,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.closeSmall(
color: _streamChatThemeData.colorTheme.grey,
color: _streamChatThemeData.colorTheme.textLowEmphasis,
),
),
title: context.translations.cancelLabel,
@@ -225,7 +225,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
question: context.translations.deleteConversationQuestion,
cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.delete(
color: _streamChatThemeData.colorTheme.accentRed,
color: _streamChatThemeData.colorTheme.accentError,
),
);
final channel = _streamChannelState.channel;
@@ -243,7 +243,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
question: context.translations.leaveConversationQuestion,
cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.userRemove(
color: _streamChatThemeData.colorTheme.accentRed,
color: _streamChatThemeData.colorTheme.accentError,
),
);
if (res == true) {
@@ -99,7 +99,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
final Widget? leading;
/// AppBar actions
/// By default it shows the [ChannelImage]
/// By default it shows the [ChannelAvatar]
final List<Widget>? actions;
@override
@@ -148,7 +148,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
Padding(
padding: const EdgeInsets.only(right: 10),
child: Center(
child: ChannelImage(
child: ChannelAvatar(
borderRadius: chatThemeData.channelTheme
.channelHeaderTheme.avatarTheme?.borderRadius,
constraints: chatThemeData.channelTheme
@@ -1,204 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/group_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image_paint.png)
///
/// It shows the current [Channel] image.
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final StreamChatClient client;
/// final Channel channel;
///
/// MyApp(this.client, this.channel);
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// debugShowCheckedModeBanner: false,
/// home: StreamChat(
/// client: client,
/// child: StreamChannel(
/// channel: channel,
/// child: Center(
/// child: ChannelImage(
/// channel: channel,
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// The widget uses a [StreamBuilder] to render the channel information
/// image as soon as it updates.
///
/// By default the widget radius size is 40x40 pixels.
/// Set the property [constraints] to set a custom dimension.
///
/// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance.
class ChannelImage extends StatelessWidget {
/// Instantiate a new ChannelImage
const ChannelImage({
Key? key,
this.channel,
this.constraints,
this.onTap,
this.borderRadius,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
}) : super(key: key);
/// [BorderRadius] to display the widget
final BorderRadius? borderRadius;
/// The channel to show the image of
final Channel? channel;
/// The diameter of the image
final BoxConstraints? constraints;
/// The function called when the image is tapped
final VoidCallback? onTap;
/// If image is selected
final bool selected;
/// Selection color for image
final Color? selectionColor;
/// Thickness of selection image
final double selectionThickness;
@override
Widget build(BuildContext context) {
final streamChat = StreamChat.of(context);
final channel = this.channel ?? StreamChannel.of(context).channel;
return BetterStreamBuilder<Map<String, dynamic>>(
stream: channel.extraDataStream,
initialData: channel.extraData,
builder: (context, data) {
String? image;
final chatThemeData = StreamChatTheme.of(context);
if (data.containsKey('image') == true) {
image = data['image'];
} else if (channel.state?.members.length == 2) {
final otherMember = channel.state?.members
.firstWhere((member) => member.user?.id != streamChat.user?.id);
return BetterStreamBuilder<User?>(
stream: streamChat.client.state.usersStream
.map((users) =>
users[otherMember?.userId] ?? otherMember!.user!)
.distinct(),
initialData: otherMember!.user,
builder: (context, user) => UserAvatar(
borderRadius: borderRadius ??
chatThemeData
.channelPreviewTheme.avatarTheme?.borderRadius,
user: user ?? otherMember.user!,
constraints: constraints ??
chatThemeData
.channelPreviewTheme.avatarTheme?.constraints,
onTap: onTap != null ? (_) => onTap!() : null,
selected: selected,
selectionColor:
selectionColor ?? chatThemeData.colorTheme.accentBlue,
selectionThickness: selectionThickness,
));
} else {
final images = channel.state?.members
.where((member) =>
member.user?.id != streamChat.user?.id &&
member.user?.extraData['image'] != null)
.take(4)
// ignore: cast_nullable_to_non_nullable
.map((e) => e.user?.extraData['image'] as String)
.toList();
return GroupImage(
images: images ?? [],
borderRadius: borderRadius ??
chatThemeData.channelPreviewTheme.avatarTheme?.borderRadius,
constraints: constraints ??
chatThemeData.channelPreviewTheme.avatarTheme?.constraints,
onTap: onTap,
selected: selected,
selectionColor:
selectionColor ?? chatThemeData.colorTheme.accentBlue,
selectionThickness: selectionThickness,
);
}
Widget child = ClipRRect(
borderRadius: borderRadius ??
chatThemeData.channelPreviewTheme.avatarTheme?.borderRadius,
child: Container(
constraints: constraints ??
chatThemeData.channelPreviewTheme.avatarTheme?.constraints,
decoration: BoxDecoration(
color: chatThemeData.colorTheme.accentBlue,
),
child: Stack(
alignment: Alignment.center,
fit: StackFit.expand,
children: <Widget>[
if (image != null)
CachedNetworkImage(
imageUrl: image,
errorWidget: (_, __, ___) => Center(
child: Text(
data.containsKey('name') ? data['name'][0] : '',
style: TextStyle(
color: chatThemeData.colorTheme.white,
fontWeight: FontWeight.bold,
),
),
),
fit: BoxFit.cover,
)
else
chatThemeData.defaultChannelImage(
context,
channel,
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
),
),
],
),
),
);
if (selected) {
child = ClipRRect(
key: const Key('selectedImage'),
borderRadius: (borderRadius ??
chatThemeData.ownMessageTheme.avatarTheme?.borderRadius ??
BorderRadius.zero) +
BorderRadius.circular(selectionThickness),
child: Container(
constraints: constraints ??
chatThemeData.ownMessageTheme.avatarTheme?.constraints,
color: selectionColor ?? chatThemeData.colorTheme.accentBlue,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: child,
),
),
);
}
return child;
},
);
}
}
@@ -148,7 +148,7 @@ class ChannelInfo extends StatelessWidget {
child: Text(
context.translations.tryAgainLabel,
style: textStyle?.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
),
),
),
@@ -154,7 +154,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
Color? color;
switch (status) {
case ConnectionStatus.connected:
color = chatThemeData.colorTheme.accentBlue;
color = chatThemeData.colorTheme.accentPrimary;
break;
case ConnectionStatus.connecting:
color = Colors.grey;
@@ -209,7 +209,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
return Text(
context.translations.streamChatLabel,
style: chatThemeData.textTheme.headlineBold.copyWith(
color: chatThemeData.colorTheme.black,
color: chatThemeData.colorTheme.textHighEmphasis,
),
);
}
@@ -262,7 +262,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
style: chatThemeData.channelListHeaderTheme.title?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
color: chatThemeData.colorTheme.accentBlue,
color: chatThemeData.colorTheme.accentPrimary,
),
),
),
@@ -239,39 +239,32 @@ class _ChannelListViewState extends State<ChannelListView> {
}
Widget _buildListView(BuildContext context, List<Channel> channels) {
late Widget child;
if (channels.isNotEmpty) {
if (widget.crossAxisCount > 1) {
child = GridView.builder(
padding: widget.padding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
),
itemCount: channels.length,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) =>
_gridItemBuilder(context, index, channels),
);
} else {
child = ListView.separated(
padding: widget.padding,
physics: const AlwaysScrollableScrollPhysics(),
itemCount:
channels.isNotEmpty ? channels.length + 1 : channels.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder!(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) =>
_listItemBuilder(context, index, channels),
);
}
if (widget.crossAxisCount > 1) {
return GridView.builder(
padding: widget.padding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
),
itemCount: channels.length,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) =>
_gridItemBuilder(context, index, channels),
);
}
return child;
return ListView.separated(
padding: widget.padding,
physics: const AlwaysScrollableScrollPhysics(),
// all channels + progress loader
itemCount: channels.length + 1,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder!(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) =>
_listItemBuilder(context, index, channels),
);
}
Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder(
@@ -292,7 +285,7 @@ class _ChannelListViewState extends State<ChannelListView> {
padding: const EdgeInsets.all(8),
child: StreamSvgIcon.message(
size: 136,
color: chatThemeData.colorTheme.greyGainsboro,
color: chatThemeData.colorTheme.disabled,
),
),
Padding(
@@ -311,7 +304,7 @@ class _ChannelListViewState extends State<ChannelListView> {
context.translations.sendingFirstMessageLabel,
textAlign: TextAlign.center,
style: chatThemeData.textTheme.body.copyWith(
color: chatThemeData.colorTheme.grey,
color: chatThemeData.colorTheme.textLowEmphasis,
),
),
),
@@ -329,7 +322,7 @@ class _ChannelListViewState extends State<ChannelListView> {
child: Text(
context.translations.startAChatLabel,
style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentBlue,
color: chatThemeData.colorTheme.accentPrimary,
),
),
),
@@ -364,8 +357,8 @@ class _ChannelListViewState extends State<ChannelListView> {
final chatThemeData = StreamChatTheme.of(context);
if (widget.crossAxisCount > 1) {
return Shimmer.fromColors(
baseColor: chatThemeData.colorTheme.greyGainsboro,
highlightColor: chatThemeData.colorTheme.whiteSmoke,
baseColor: chatThemeData.colorTheme.disabled,
highlightColor: chatThemeData.colorTheme.inputBg,
child: Column(
children: [
const SizedBox(height: 4),
@@ -393,12 +386,12 @@ class _ChannelListViewState extends State<ChannelListView> {
);
} else {
return Shimmer.fromColors(
baseColor: chatThemeData.colorTheme.greyGainsboro,
highlightColor: chatThemeData.colorTheme.whiteSmoke,
baseColor: chatThemeData.colorTheme.disabled,
highlightColor: chatThemeData.colorTheme.inputBg,
child: ListTile(
leading: Container(
decoration: BoxDecoration(
color: chatThemeData.colorTheme.white,
color: chatThemeData.colorTheme.barsBg,
shape: BoxShape.circle,
),
constraints: const BoxConstraints.tightFor(
@@ -414,7 +407,7 @@ class _ChannelListViewState extends State<ChannelListView> {
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: chatThemeData.colorTheme.white,
color: chatThemeData.colorTheme.barsBg,
borderRadius: BorderRadius.circular(11),
),
constraints: const BoxConstraints.tightFor(
@@ -431,7 +424,7 @@ class _ChannelListViewState extends State<ChannelListView> {
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: chatThemeData.colorTheme.white,
color: chatThemeData.colorTheme.barsBg,
borderRadius: BorderRadius.circular(11),
),
constraints: const BoxConstraints.expand(
@@ -443,7 +436,7 @@ class _ChannelListViewState extends State<ChannelListView> {
Container(
margin: const EdgeInsets.only(left: 16),
decoration: BoxDecoration(
color: chatThemeData.colorTheme.white,
color: chatThemeData.colorTheme.barsBg,
borderRadius: BorderRadius.circular(11),
),
constraints: const BoxConstraints.tightFor(
@@ -488,70 +481,73 @@ class _ChannelListViewState extends State<ChannelListView> {
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
final channelsBloc = ChannelsBloc.of(context);
if (i == channels.length) {
return _buildQueryProgressIndicator(context, channelsBloc);
}
final onTap = _getChannelTap(context);
final chatThemeData = StreamChatTheme.of(context);
final backgroundColor = chatThemeData.colorTheme.whiteSmoke;
final backgroundColor = chatThemeData.colorTheme.inputBg;
final channel = channels[i];
if (i < channels.length) {
final channel = channels[i];
return StreamChannel(
key: ValueKey<String>('CHANNEL-${channel.cid}'),
channel: channel,
child: Slidable(
controller: _slideController,
enabled: widget.swipeToAction,
actionPane: const SlidableBehindActionPane(),
actionExtentRatio: 0.12,
secondaryActions: widget.swipeActions
?.map((e) => IconSlideAction(
color: e.color,
iconWidget: e.iconWidget,
onTap: () {
e.onTap?.call(channel);
},
))
.toList() ??
<Widget>[
return StreamChannel(
key: ValueKey<String>('CHANNEL-${channel.cid}'),
channel: channel,
child: Slidable(
controller: _slideController,
enabled: widget.swipeToAction,
actionPane: const SlidableBehindActionPane(),
actionExtentRatio: 0.12,
secondaryActions: widget.swipeActions
?.map((e) => IconSlideAction(
color: e.color,
iconWidget: e.iconWidget,
onTap: () {
e.onTap?.call(channel);
},
))
.toList() ??
<Widget>[
IconSlideAction(
color: backgroundColor,
icon: Icons.more_horiz,
onTap: widget.onMoreDetailsPressed != null
? () {
widget.onMoreDetailsPressed!(channel);
}
: () {
showModalBottomSheet(
clipBehavior: Clip.hardEdge,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
context: context,
builder: (context) => StreamChannel(
channel: channel,
child: ChannelBottomSheet(
onViewInfoTap: () {
widget.onViewInfoTap?.call(channel);
},
),
),
);
},
),
if ([
'admin',
'owner',
].contains(channel.state!.members
.firstWhereOrNull(
(m) => m.userId == channel.client.state.user?.id)
?.role))
IconSlideAction(
color: backgroundColor,
icon: Icons.more_horiz,
onTap: widget.onMoreDetailsPressed != null
? () {
widget.onMoreDetailsPressed!(channel);
}
: () {
showModalBottomSheet(
clipBehavior: Clip.hardEdge,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
context: context,
builder: (context) => StreamChannel(
channel: channel,
child: ChannelBottomSheet(
onViewInfoTap: () {
widget.onViewInfoTap?.call(channel);
},
),
),
);
},
),
if ([
'admin',
'owner',
].contains(channel.state!.members
.firstWhereOrNull(
(m) => m.userId == channel.client.state.user?.id)
?.role))
IconSlideAction(
color: backgroundColor,
iconWidget: StreamSvgIcon.delete(
color: chatThemeData.colorTheme.accentRed,
iconWidget: StreamSvgIcon.delete(
color: chatThemeData.colorTheme.accentError,
),
onTap: widget.onDeletePressed != null
? () {
@@ -563,36 +559,33 @@ class _ChannelListViewState extends State<ChannelListView> {
title:
context.translations.deleteConversationLabel,
question: context
.translations.deleteConversationQuestion,
.translations.deleteConversationQuestion,
okText: context.translations.deleteLabel,
cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.delete(
color: chatThemeData.colorTheme.accentRed,
),
);
if (res == true) {
await channel.delete();
}
},
),
],
child: DecoratedBox(
decoration: BoxDecoration(
color: chatThemeData.colorTheme.whiteSnow,
),
child: widget.channelPreviewBuilder?.call(context, channel) ??
ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: () => widget.onImageTap?.call(channel),
onTap: (channel) => onTap(channel, widget.channelWidget),
cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.delete(
color: chatThemeData.colorTheme.accentError,
),
);
if (res == true) {
await channel.delete();
}
},
),
],
child: DecoratedBox(
decoration: BoxDecoration(
color: chatThemeData.colorTheme.appBg,
),
child: widget.channelPreviewBuilder?.call(context, channel) ??
ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: () => widget.onImageTap?.call(channel),
onTap: (channel) => onTap(channel, widget.channelWidget),
),
),
);
} else {
return _buildQueryProgressIndicator(context, channelsBloc);
}
),
);
}
ChannelTapCallback _getChannelTap(BuildContext context) {
@@ -628,7 +621,7 @@ class _ChannelListViewState extends State<ChannelListView> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ChannelImage(
ChannelAvatar(
channel: channel,
borderRadius: BorderRadius.circular(32),
selected: selected,
@@ -661,28 +654,33 @@ class _ChannelListViewState extends State<ChannelListView> {
ChannelsBlocState channelsProvider,
) =>
BetterStreamBuilder<bool>(
stream: channelsProvider.queryChannelsLoading,
initialData: false,
errorBuilder: (context, err) => Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text(context.translations.loadingChannelsError),
),
stream: channelsProvider.queryChannelsLoading,
initialData: false,
errorBuilder: (context, err) {
final theme = StreamChatTheme.of(context);
return Container(
color: theme.colorTheme.textLowEmphasis.withOpacity(0.9),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
context.translations.loadingChannelsError,
style: theme.textTheme.body.copyWith(
color: Colors.white,
),
),
builder: (context, data) => data
? const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
)
: const Offstage());
),
);
},
builder: (context, showLoading) {
if (!showLoading) return const Offstage();
return const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
);
},
);
Widget _separatorBuilder(context, i) {
final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
@@ -56,7 +56,7 @@ class ChannelPreview extends StatelessWidget {
final Widget? subtitle;
/// Widget rendering the leading element, by default
/// it shows the [ChannelImage]
/// it shows the [ChannelAvatar]
final Widget? leading;
/// Widget rendering the trailing element,
@@ -82,20 +82,9 @@ class ChannelPreview extends StatelessWidget {
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
onTap: () {
if (onTap != null) {
onTap!(channel);
}
},
onLongPress: () {
if (onLongPress != null) {
onLongPress!(channel);
}
},
leading: leading ??
ChannelImage(
onTap: onImageTap,
),
onTap: () => onTap?.call(channel),
onLongPress: () => onLongPress?.call(channel),
leading: leading ?? ChannelAvatar(onTap: onImageTap),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
@@ -56,7 +56,7 @@ class DateDivider extends StatelessWidget {
child: Text(
dayInfo,
style: chatThemeData.textTheme.footnote.copyWith(
color: chatThemeData.colorTheme.white,
color: chatThemeData.colorTheme.barsBg,
),
),
),
@@ -40,8 +40,8 @@ class DeletedMessage extends StatelessWidget {
side: borderSide ??
BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? chatThemeData.colorTheme.white.withAlpha(24)
: chatThemeData.colorTheme.black.withAlpha(24),
? chatThemeData.colorTheme.barsBg.withAlpha(24)
: chatThemeData.colorTheme.textHighEmphasis.withAlpha(24),
),
),
child: Padding(
@@ -6,8 +6,8 @@ import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/src/image_footer.dart';
import 'package:stream_chat_flutter/src/image_header.dart';
import 'package:stream_chat_flutter/src/gallery_footer.dart';
import 'package:stream_chat_flutter/src/gallery_header.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'package:stream_chat_flutter/src/extension.dart';
@@ -181,7 +181,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ImageHeader(
GalleryHeader(
userName: widget.userName,
// TODO: Fix this
sentAt: context.translations.sentAtText(
@@ -201,7 +201,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
},
),
if (widget.message.type != 'ephemeral')
ImageFooter(
GalleryFooter(
currentPage: _currentPage,
totalPages: widget.mediaAttachments.length,
mediaAttachments: widget.mediaAttachments,
@@ -14,9 +14,9 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Footer widget for media display
class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
/// Creates a channel header
const ImageFooter({
const GalleryFooter({
Key? key,
required this.message,
this.onBackPressed,
@@ -55,13 +55,13 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
final ValueChanged<int>? mediaSelectedCallBack;
@override
_ImageFooterState createState() => _ImageFooterState();
_GalleryFooterState createState() => _GalleryFooterState();
@override
final Size preferredSize;
}
class _ImageFooterState extends State<ImageFooter> {
class _GalleryFooterState extends State<GalleryFooter> {
final TextEditingController _messageController = TextEditingController();
final FocusNode _messageFocusNode = FocusNode();
@@ -79,7 +79,7 @@ class _ImageFooterState extends State<ImageFooter> {
Widget build(BuildContext context) {
const showShareButton = !kIsWeb;
final mediaQueryData = MediaQuery.of(context);
final chatThemeData = StreamChatTheme.of(context);
final galleryFooterThemeData = GalleryFooterTheme.of(context);
return SizedBox.fromSize(
size: Size(
mediaQueryData.size.width,
@@ -89,7 +89,7 @@ class _ImageFooterState extends State<ImageFooter> {
context: context,
removeTop: true,
child: BottomAppBar(
color: chatThemeData.colorTheme.white,
color: galleryFooterThemeData.backgroundColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -101,7 +101,7 @@ class _ImageFooterState extends State<ImageFooter> {
IconButton(
icon: StreamSvgIcon.iconShare(
size: 24,
color: chatThemeData.colorTheme.black,
color: galleryFooterThemeData.shareIconColor,
),
onPressed: () async {
final attachment =
@@ -137,7 +137,7 @@ class _ImageFooterState extends State<ImageFooter> {
children: <Widget>[
Text(
'${widget.currentPage + 1} of ${widget.totalPages}',
style: chatThemeData.textTheme.headlineBold,
style: galleryFooterThemeData.titleTextStyle,
),
],
),
@@ -145,7 +145,7 @@ class _ImageFooterState extends State<ImageFooter> {
),
IconButton(
icon: StreamSvgIcon.iconGrid(
color: chatThemeData.colorTheme.black,
color: galleryFooterThemeData.gridIconButtonColor,
),
onPressed: () => _showPhotosModal(context),
),
@@ -158,10 +158,11 @@ class _ImageFooterState extends State<ImageFooter> {
void _showPhotosModal(context) {
final chatThemeData = StreamChatTheme.of(context);
final galleryFooterThemeData = GalleryFooterTheme.of(context);
showModalBottomSheet(
context: context,
barrierColor: chatThemeData.colorTheme.overlay,
backgroundColor: chatThemeData.colorTheme.white,
barrierColor: galleryFooterThemeData.bottomSheetBarrierColor,
backgroundColor: galleryFooterThemeData.bottomSheetBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
@@ -192,7 +193,8 @@ class _ImageFooterState extends State<ImageFooter> {
padding: const EdgeInsets.all(16),
child: Text(
context.translations.photosLabel,
style: chatThemeData.textTheme.headlineBold,
style:
galleryFooterThemeData.bottomSheetPhotosTextStyle,
),
),
),
@@ -200,7 +202,8 @@ class _ImageFooterState extends State<ImageFooter> {
alignment: Alignment.centerRight,
child: IconButton(
icon: StreamSvgIcon.close(
color: chatThemeData.colorTheme.black,
color:
galleryFooterThemeData.bottomSheetCloseIconColor,
),
onPressed: () => Navigator.maybePop(context),
),
@@ -263,7 +266,8 @@ class _ImageFooterState extends State<ImageFooter> {
boxShadow: [
BoxShadow(
blurRadius: 8,
color: chatThemeData.colorTheme.black
color: chatThemeData
.colorTheme.textHighEmphasis
.withOpacity(0.3),
),
],
@@ -5,9 +5,9 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Header/AppBar widget for media display screen
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
/// Creates a channel header
const ImageHeader({
const GalleryHeader({
Key? key,
required this.message,
this.currentIndex = 0,
@@ -51,7 +51,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
return AppBar(
textTheme: Theme.of(context).textTheme,
brightness: Theme.of(context).brightness,
@@ -59,18 +59,18 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
leading: showBackButton
? IconButton(
icon: StreamSvgIcon.close(
color: chatThemeData.colorTheme.black,
color: galleryHeaderThemeData.closeButtonColor,
size: 24,
),
onPressed: onBackPressed,
)
: const SizedBox(),
backgroundColor: chatThemeData.channelTheme.channelHeaderTheme.color,
backgroundColor: galleryHeaderThemeData.backgroundColor,
actions: <Widget>[
if (message.type != 'ephemeral')
IconButton(
icon: StreamSvgIcon.iconMenuPoint(
color: chatThemeData.colorTheme.black,
color: galleryHeaderThemeData.iconMenuPointColor,
),
onPressed: () {
_showMessageActionModalBottomSheet(context);
@@ -90,11 +90,11 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
children: <Widget>[
Text(
userName,
style: chatThemeData.textTheme.headlineBold,
style: galleryHeaderThemeData.titleTextStyle,
),
Text(
sentAt,
style: chatThemeData.channelPreviewTheme.subtitle,
style: galleryHeaderThemeData.subtitleTextStyle,
),
],
),
@@ -109,11 +109,13 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
void _showMessageActionModalBottomSheet(BuildContext context) async {
final channel = StreamChannel.of(context).channel;
final galleryHeaderThemeData =
StreamChatTheme.of(context).galleryHeaderTheme;
final result = await showDialog(
useRootNavigator: false,
context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
builder: (context) => StreamChannel(
channel: channel,
child: AttachmentActionsModal(
@@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget for constructing a group of images
class GroupAvatar extends StatelessWidget {
/// Constructor for creating a [GroupAvatar]
const GroupAvatar({
Key? key,
required this.members,
this.constraints,
this.onTap,
this.borderRadius,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
}) : super(key: key);
/// List of images to display
final List<Member> members;
/// Constraints on the widget
final BoxConstraints? constraints;
/// Callback when widget is tapped
final VoidCallback? onTap;
/// Highlights if selected
final bool selected;
/// [BorderRadius] to pass to the widget
final BorderRadius? borderRadius;
/// Color of selection if selected
final Color? selectionColor;
/// Thickness with which color of selection is shown
final double selectionThickness;
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
assert(channel.state != null, 'Channel ${channel.id} is not initialized');
final streamChatTheme = StreamChatTheme.of(context);
final colorTheme = streamChatTheme.colorTheme;
final previewTheme = streamChatTheme.channelPreviewTheme.avatarTheme;
Widget avatar = GestureDetector(
onTap: onTap,
child: ClipRRect(
borderRadius: borderRadius ?? previewTheme?.borderRadius,
child: Container(
constraints: constraints ?? previewTheme?.constraints,
decoration: BoxDecoration(color: colorTheme.accentPrimary),
child: Flex(
direction: Axis.vertical,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
fit: FlexFit.tight,
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: members
.take(2)
.map(
(member) => Flexible(
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.cover,
clipBehavior: Clip.antiAlias,
child: Transform.scale(
scale: 1.2,
child: BetterStreamBuilder<Member>(
stream: channel.state!.membersStream.map(
(members) => members.firstWhere(
(it) => it.userId == member.userId,
orElse: () => member,
),
),
initialData: member,
builder: (context, member) => UserAvatar(
user: member.user!,
borderRadius: BorderRadius.zero,
),
),
),
),
),
)
.toList(),
),
),
if (members.length > 2)
Flexible(
fit: FlexFit.tight,
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: members
.skip(2)
.take(2)
.map(
(member) => Flexible(
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.cover,
clipBehavior: Clip.antiAlias,
child: Transform.scale(
scale: 1.2,
child: BetterStreamBuilder<Member>(
stream: channel.state!.membersStream.map(
(members) => members.firstWhere(
(it) => it.userId == member.userId,
orElse: () => member,
),
),
initialData: member,
builder: (context, member) => UserAvatar(
user: member.user!,
borderRadius: BorderRadius.zero,
),
),
),
),
),
)
.toList(),
),
),
],
),
),
),
);
if (selected) {
avatar = ClipRRect(
borderRadius: BorderRadius.circular(selectionThickness) +
(borderRadius ?? previewTheme?.borderRadius ?? BorderRadius.zero),
child: Container(
constraints: constraints ?? previewTheme?.constraints,
color: selectionColor ?? colorTheme.accentPrimary,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: avatar,
),
),
);
}
return avatar;
}
}
@@ -1,135 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget for constructing a group of images
class GroupImage extends StatelessWidget {
/// Constructor for creating a [GroupImage]
const GroupImage({
Key? key,
required this.images,
this.constraints,
this.onTap,
this.borderRadius,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
}) : super(key: key);
/// List of images to display
final List<String> images;
/// Constraints on the widget
final BoxConstraints? constraints;
/// Callback when widget is tapped
final VoidCallback? onTap;
/// Highlights if selected
final bool selected;
/// [BorderRadius] to pass to the widget
final BorderRadius? borderRadius;
/// Color of selection if selected
final Color? selectionColor;
/// Thickness with which color of selection is shown
final double selectionThickness;
@override
Widget build(BuildContext context) {
Widget? avatar;
final streamChatTheme = StreamChatTheme.of(context);
avatar = GestureDetector(
onTap: onTap,
child: ClipRRect(
borderRadius: borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
child: Container(
constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme?.constraints,
decoration: BoxDecoration(
color: streamChatTheme.colorTheme.accentBlue,
),
child: Flex(
direction: Axis.vertical,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
fit: FlexFit.tight,
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: images
.take(2)
.map((url) => Flexible(
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.cover,
clipBehavior: Clip.antiAlias,
child: Transform.scale(
scale: 1.2,
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.cover,
),
),
),
))
.toList(),
),
),
if (images.length > 2)
Flexible(
fit: FlexFit.tight,
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: images
.skip(2)
.map((url) => Flexible(
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.cover,
clipBehavior: Clip.antiAlias,
child: Transform.scale(
scale: 1.2,
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.cover,
),
),
),
))
.toList(),
),
),
],
),
),
),
);
if (selected) {
avatar = ClipRRect(
borderRadius: (borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius ??
BorderRadius.zero) +
BorderRadius.circular(selectionThickness),
child: Container(
color: selectionColor ?? streamChatTheme.colorTheme.accentBlue,
height: 64,
width: 64,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: avatar,
),
),
);
}
return avatar;
}
}
@@ -46,8 +46,8 @@ class InfoTile extends StatelessWidget {
childAnchor: childAnchor ?? Alignment.bottomCenter,
portal: Container(
height: 25,
color:
backgroundColor ?? chatThemeData.colorTheme.grey.withOpacity(0.9),
color: backgroundColor ??
chatThemeData.colorTheme.textLowEmphasis.withOpacity(0.9),
child: Center(
child: Text(
message,
@@ -90,8 +90,8 @@ class _MediaListViewState extends State<MediaListView> {
? 1.0
: 0.0,
child: Container(
color:
chatThemeData.colorTheme.black.withOpacity(0.5),
color: chatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5),
alignment: Alignment.topRight,
padding: const EdgeInsets.only(
top: 8,
@@ -99,10 +99,11 @@ class _MediaListViewState extends State<MediaListView> {
),
child: CircleAvatar(
radius: 12,
backgroundColor: chatThemeData.colorTheme.white,
backgroundColor: chatThemeData.colorTheme.barsBg,
child: StreamSvgIcon.check(
size: 24,
color: chatThemeData.colorTheme.black,
color:
chatThemeData.colorTheme.textHighEmphasis,
),
),
),
@@ -124,7 +125,7 @@ class _MediaListViewState extends State<MediaListView> {
child: Text(
media.videoDuration.format(),
style: TextStyle(
color: chatThemeData.colorTheme.white,
color: chatThemeData.colorTheme.barsBg,
),
),
),
@@ -76,7 +76,7 @@ class MentionTile extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: chatThemeData.textTheme.footnoteBold.copyWith(
color: chatThemeData.colorTheme.grey,
color: chatThemeData.colorTheme.textLowEmphasis,
),
),
],
@@ -90,7 +90,7 @@ class MentionTile extends StatelessWidget {
left: 8,
),
child: StreamSvgIcon.mentions(
color: chatThemeData.colorTheme.accentBlue,
color: chatThemeData.colorTheme.accentPrimary,
),
),
],
@@ -165,7 +165,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
child: SizedBox(
width: mediaQueryData.size.width * 0.75,
child: Material(
color: streamChatThemeData.colorTheme.whiteSnow,
color: streamChatThemeData.colorTheme.appBg,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@@ -197,7 +197,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
].insertBetween(
Container(
height: 1,
color: streamChatThemeData.colorTheme.greyWhisper,
color: streamChatThemeData.colorTheme.borders,
),
),
),
@@ -270,7 +270,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
context,
title: context.translations.flagMessageLabel,
icon: StreamSvgIcon.flag(
color: streamChatThemeData.colorTheme.accentRed,
color: streamChatThemeData.colorTheme.accentError,
size: 24,
),
question: context.translations.flagMessageQuestion,
@@ -285,7 +285,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
await showInfoDialog(
context,
icon: StreamSvgIcon.flag(
color: theme.colorTheme.accentRed,
color: theme.colorTheme.accentError,
size: 24,
),
details: context.translations.flagMessageSuccessfulText,
@@ -298,7 +298,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
await showInfoDialog(
context,
icon: StreamSvgIcon.flag(
color: theme.colorTheme.accentRed,
color: theme.colorTheme.accentError,
size: 24,
),
details: context.translations.flagMessageSuccessfulText,
@@ -335,7 +335,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
context,
title: context.translations.deleteMessageLabel,
icon: StreamSvgIcon.flag(
color: StreamChatTheme.of(context).colorTheme.accentRed,
color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24,
),
question: context.translations.deleteMessageQuestion,
@@ -361,7 +361,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
showInfoDialog(
context,
icon: StreamSvgIcon.error(
color: StreamChatTheme.of(context).colorTheme.accentRed,
color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24,
),
details: context.translations.operationCouldNotBeCompletedText,
@@ -542,7 +542,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
child: Row(
children: [
StreamSvgIcon.circleUp(
color: streamChatThemeData.colorTheme.accentBlue,
color: streamChatThemeData.colorTheme.accentPrimary,
),
const SizedBox(width: 16),
Text(
@@ -589,7 +589,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
Padding(
padding: const EdgeInsets.all(8),
child: StreamSvgIcon.edit(
color: streamChatThemeData.colorTheme.greyGainsboro,
color: streamChatThemeData.colorTheme.disabled,
),
),
Text(
@@ -26,6 +26,18 @@ import 'package:substring_highlight/substring_highlight.dart';
import 'package:video_compress/video_compress.dart';
import 'package:stream_chat_flutter/src/extension.dart';
export 'package:video_compress/video_compress.dart' show VideoQuality;
/// A callback that can be passed to [MessageInput.onError].
///
/// This callback should not throw.
///
/// It exists merely for error reporting, and should not be used otherwise.
typedef ErrorListener = void Function(
Object error,
StackTrace? stackTrace,
);
/// Builder for attachment thumbnails
typedef AttachmentThumbnailBuilder = Widget Function(
BuildContext,
@@ -150,11 +162,20 @@ class MessageInput extends StatefulWidget {
this.showCommandsButton = true,
this.mentionsTileBuilder,
this.maxAttachmentSize = _kDefaultMaxAttachmentSize,
this.compressedVideoQuality = VideoQuality.DefaultQuality,
this.compressedVideoFrameRate = 30,
this.onError,
}) : super(key: key);
/// Message to edit
final Message? editMessage;
/// Video quality to use when compressing the videos
final VideoQuality compressedVideoQuality;
/// Frame rate to use when compressing the videos
final int compressedVideoFrameRate;
/// Max attachment size in bytes
/// Defaults to 20 MB
/// do not set it if you're using our default CDN
@@ -224,6 +245,9 @@ class MessageInput extends StatefulWidget {
/// Customize the tile for the mentions overlay
final MentionTileBuilder? mentionsTileBuilder;
/// A callback for error reporting
final ErrorListener? onError;
@override
MessageInputState createState() => MessageInputState();
@@ -331,7 +355,7 @@ class MessageInputState extends State<MessageInput> {
Padding(
padding: const EdgeInsets.all(8),
child: StreamSvgIcon.reply(
color: _streamChatTheme.colorTheme.greyGainsboro,
color: _streamChatTheme.colorTheme.disabled,
),
),
const Text(
@@ -398,7 +422,8 @@ class MessageInputState extends State<MessageInput> {
border: _sendAsDm
? null
: Border.all(
color: _streamChatTheme.colorTheme.black.withOpacity(.5),
color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(.5),
width: 2,
),
borderRadius: BorderRadius.circular(3),
@@ -407,8 +432,8 @@ class MessageInputState extends State<MessageInput> {
child: Material(
borderRadius: BorderRadius.circular(3),
color: _sendAsDm
? _streamChatTheme.colorTheme.accentBlue
: _streamChatTheme.colorTheme.white,
? _streamChatTheme.colorTheme.accentPrimary
: _streamChatTheme.colorTheme.barsBg,
child: InkWell(
onTap: () {
setState(() {
@@ -423,7 +448,7 @@ class MessageInputState extends State<MessageInput> {
: CrossFadeState.showSecond,
firstChild: StreamSvgIcon.check(
size: 16,
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
),
secondChild: const SizedBox(
height: 16,
@@ -439,7 +464,8 @@ class MessageInputState extends State<MessageInput> {
child: Text(
context.translations.alsoSendAsDirectMessageLabel,
style: _streamChatTheme.textTheme.footnote.copyWith(
color: _streamChatTheme.colorTheme.black.withOpacity(0.5),
color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(0.5),
),
),
),
@@ -579,7 +605,7 @@ class MessageInputState extends State<MessageInput> {
isDense: true,
hintText: _getHint(context),
hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith(
color: _streamChatTheme.colorTheme.grey,
color: _streamChatTheme.colorTheme.textLowEmphasis,
),
border: const OutlineInputBorder(
borderSide: BorderSide(
@@ -617,7 +643,7 @@ class MessageInputState extends State<MessageInput> {
constraints: BoxConstraints.tight(const Size(64, 24)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: _streamChatTheme.colorTheme.accentBlue,
color: _streamChatTheme.colorTheme.accentPrimary,
),
alignment: Alignment.center,
child: Row(
@@ -831,12 +857,12 @@ class MessageInputState extends State<MessageInput> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
clipBehavior: Clip.hardEdge,
child: Container(
constraints: BoxConstraints.loose(const Size.fromHeight(400)),
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
borderRadius: BorderRadius.circular(8)),
child: ListView(
padding: const EdgeInsets.all(0),
@@ -852,14 +878,14 @@ class MessageInputState extends State<MessageInput> {
horizontal: 8,
),
child: StreamSvgIcon.lightning(
color: _streamChatTheme.colorTheme.accentBlue,
color: _streamChatTheme.colorTheme.accentPrimary,
),
),
Text(
context.translations.instantCommandsLabel,
style: TextStyle(
color:
_streamChatTheme.colorTheme.black.withOpacity(.5),
color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(.5),
),
)
],
@@ -899,7 +925,7 @@ class MessageInputState extends State<MessageInput> {
color: _streamChatTheme
// ignore: lines_longer_than_80_chars
.colorTheme
.grey,
.textLowEmphasis,
),
),
],
@@ -943,24 +969,29 @@ class MessageInputState extends State<MessageInput> {
switch (index) {
case 0:
return _attachments.isEmpty
? streamChatThemeData.colorTheme.accentBlue
? streamChatThemeData.colorTheme.accentPrimary
: (!_attachmentContainsFile
? streamChatThemeData.colorTheme.accentBlue
: streamChatThemeData.colorTheme.black.withOpacity(0.2));
? streamChatThemeData.colorTheme.accentPrimary
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2));
case 1:
return _attachmentContainsFile
? streamChatThemeData.colorTheme.accentBlue
? streamChatThemeData.colorTheme.accentPrimary
: (_attachments.isEmpty
? streamChatThemeData.colorTheme.black.withOpacity(0.5)
: streamChatThemeData.colorTheme.black.withOpacity(0.2));
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2));
case 2:
return _attachmentContainsFile && _attachments.isNotEmpty
? streamChatThemeData.colorTheme.black.withOpacity(0.2)
: streamChatThemeData.colorTheme.black.withOpacity(0.5);
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5);
case 3:
return _attachmentContainsFile && _attachments.isNotEmpty
? streamChatThemeData.colorTheme.black.withOpacity(0.2)
: streamChatThemeData.colorTheme.black.withOpacity(0.5);
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5);
default:
return Colors.black;
}
@@ -970,7 +1001,7 @@ class MessageInputState extends State<MessageInput> {
duration: const Duration(milliseconds: 300),
height: _openFilePickerSection ? _filePickerSize : 0,
child: Material(
color: _streamChatTheme.colorTheme.whiteSmoke,
color: _streamChatTheme.colorTheme.inputBg,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -1033,7 +1064,7 @@ class MessageInputState extends State<MessageInput> {
},
child: DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
@@ -1049,7 +1080,7 @@ class MessageInputState extends State<MessageInput> {
height: 4,
child: DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.whiteSmoke,
color: _streamChatTheme.colorTheme.inputBg,
borderRadius: BorderRadius.circular(4),
),
),
@@ -1063,7 +1094,7 @@ class MessageInputState extends State<MessageInput> {
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
borderRadius: BorderRadius.circular(8),
),
child: _PickerWidget(
@@ -1106,8 +1137,11 @@ class MessageInputState extends State<MessageInput> {
if (file.size! > widget.maxAttachmentSize) {
if (medium.type == AssetType.video && file.path != null) {
final mediaInfo = await (VideoService.compressVideo(file.path!)
as FutureOr<MediaInfo>);
final mediaInfo = await (VideoService.compressVideo(
file.path!,
frameRate: widget.compressedVideoFrameRate,
quality: widget.compressedVideoQuality,
) as FutureOr<MediaInfo>);
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert(
@@ -1147,7 +1181,7 @@ class MessageInputState extends State<MessageInput> {
);
case 'ban':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentBlue,
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.iconUserDelete(
size: 16,
@@ -1156,7 +1190,7 @@ class MessageInputState extends State<MessageInput> {
);
case 'flag':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentBlue,
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.flag(
size: 14,
@@ -1165,7 +1199,7 @@ class MessageInputState extends State<MessageInput> {
);
case 'imgur':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentBlue,
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: ClipOval(
child: StreamSvgIcon.imgur(
@@ -1175,7 +1209,7 @@ class MessageInputState extends State<MessageInput> {
);
case 'mute':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentBlue,
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.mute(
size: 16,
@@ -1184,7 +1218,7 @@ class MessageInputState extends State<MessageInput> {
);
case 'unban':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentBlue,
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.userAdd(
size: 16,
@@ -1193,7 +1227,7 @@ class MessageInputState extends State<MessageInput> {
);
case 'unmute':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentBlue,
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.volumeUp(
size: 16,
@@ -1202,7 +1236,7 @@ class MessageInputState extends State<MessageInput> {
);
default:
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentBlue,
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.lightning(
size: 16,
@@ -1242,7 +1276,7 @@ class MessageInputState extends State<MessageInput> {
final child = Card(
margin: const EdgeInsets.all(8),
elevation: 2,
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
@@ -1250,7 +1284,7 @@ class MessageInputState extends State<MessageInput> {
child: Container(
constraints: BoxConstraints.loose(const Size.fromHeight(240)),
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
),
child: FutureBuilder<List<Member>>(
future: queryMembers ?? Future.value(members),
@@ -1266,7 +1300,7 @@ class MessageInputState extends State<MessageInput> {
.where((it) => it.user != null)
.map(
(m) => Material(
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
child: InkWell(
onTap: () {
if (m.user != null) {
@@ -1353,7 +1387,7 @@ class MessageInputState extends State<MessageInput> {
child: Card(
margin: const EdgeInsets.all(8),
elevation: 2,
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
@@ -1368,7 +1402,7 @@ class MessageInputState extends State<MessageInput> {
offset: Offset(0, -4),
),
],
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
),
child: ListView.builder(
padding: const EdgeInsets.all(0),
@@ -1384,8 +1418,8 @@ class MessageInputState extends State<MessageInput> {
padding:
const EdgeInsets.symmetric(horizontal: 8),
child: StreamSvgIcon.smile(
color:
_streamChatTheme.colorTheme.accentBlue,
color: _streamChatTheme
.colorTheme.accentPrimary,
),
),
Flexible(
@@ -1394,7 +1428,8 @@ class MessageInputState extends State<MessageInput> {
query,
),
style: TextStyle(
color: _streamChatTheme.colorTheme.black
color: _streamChatTheme
.colorTheme.textHighEmphasis
.withOpacity(.5),
),
),
@@ -1566,11 +1601,12 @@ class MessageInputState extends State<MessageInput> {
onPressed: () {
setState(() => _attachments.remove(attachment.id));
},
fillColor: _streamChatTheme.colorTheme.black.withOpacity(.5),
fillColor:
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.5),
child: Center(
child: StreamSvgIcon.close(
size: 24,
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
),
),
),
@@ -1605,8 +1641,8 @@ class MessageInputState extends State<MessageInput> {
errorWidget: (_, obj, trace) =>
getFileTypeImage(attachment.extraData['other'] as String?),
placeholder: (context, _) => Shimmer.fromColors(
baseColor: _streamChatTheme.colorTheme.greyGainsboro,
highlightColor: _streamChatTheme.colorTheme.whiteSmoke,
baseColor: _streamChatTheme.colorTheme.disabled,
highlightColor: _streamChatTheme.colorTheme.inputBg,
child: Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
@@ -1647,7 +1683,7 @@ class MessageInputState extends State<MessageInput> {
return IconButton(
icon: StreamSvgIcon.lightning(
color: s.isNotEmpty
? _streamChatTheme.colorTheme.greyGainsboro
? _streamChatTheme.colorTheme.disabled
: (_commandsOverlay != null
? _streamChatTheme.messageInputTheme.actionButtonColor
: _streamChatTheme.messageInputTheme.actionButtonIdleColor),
@@ -1823,11 +1859,11 @@ class MessageInputState extends State<MessageInput> {
}
if (camera) {
PickedFile? pickedFile;
XFile? pickedFile;
if (fileType == DefaultAttachmentTypes.image) {
pickedFile = await _imagePicker.getImage(source: ImageSource.camera);
pickedFile = await _imagePicker.pickImage(source: ImageSource.camera);
} else if (fileType == DefaultAttachmentTypes.video) {
pickedFile = await _imagePicker.getVideo(source: ImageSource.camera);
pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera);
}
if (pickedFile == null) {
return;
@@ -1879,8 +1915,11 @@ class MessageInputState extends State<MessageInput> {
if (file.size! > widget.maxAttachmentSize) {
if (attachmentType == 'video' && file.path != null) {
final mediaInfo = await (VideoService.compressVideo(file.path!)
as FutureOr<MediaInfo>);
final mediaInfo = await (VideoService.compressVideo(
file.path!,
frameRate: widget.compressedVideoFrameRate,
quality: widget.compressedVideoQuality,
) as FutureOr<MediaInfo>);
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert(
@@ -1957,7 +1996,7 @@ class MessageInputState extends State<MessageInput> {
}
/// Sends the current message
void sendMessage() async {
Future<void> sendMessage() async {
var text = textEditingController.text.trim();
if (text.isEmpty && _attachments.isEmpty) {
return;
@@ -1973,9 +2012,7 @@ class MessageInputState extends State<MessageInput> {
textEditingController.clear();
_attachments.clear();
if (widget.onQuotedMessageCleared != null) {
widget.onQuotedMessageCleared!();
}
widget.onQuotedMessageCleared?.call();
setState(() {
_messageIsPresent = false;
@@ -1987,7 +2024,6 @@ class MessageInputState extends State<MessageInput> {
_mentionsOverlay?.remove();
_mentionsOverlay = null;
Future sendingFuture;
Message message;
if (widget.editMessage != null) {
message = widget.editMessage!.copyWith(
@@ -2025,33 +2061,39 @@ class MessageInputState extends State<MessageInput> {
_mentionedUsers.clear();
if (widget.editMessage == null ||
widget.editMessage!.status == MessageSendingStatus.failed ||
widget.editMessage!.status == MessageSendingStatus.sending) {
sendingFuture = channel.sendMessage(message);
} else {
sendingFuture = channel.updateMessage(message);
}
try {
Future sendingFuture;
if (widget.editMessage == null ||
widget.editMessage!.status == MessageSendingStatus.failed ||
widget.editMessage!.status == MessageSendingStatus.sending) {
sendingFuture = channel.sendMessage(message);
} else {
sendingFuture = channel.updateMessage(message);
}
if (!shouldUnfocus) {
FocusScope.of(context).requestFocus(_focusNode);
}
if (!shouldUnfocus) {
FocusScope.of(context).requestFocus(_focusNode);
}
return sendingFuture.then((resp) {
final resp = await sendingFuture;
if (resp.message?.type == 'error') {
_parseExistingMessage(message);
}
if (widget.onMessageSent != null) {
widget.onMessageSent!(resp.message);
widget.onMessageSent?.call(resp.message);
} catch (e, stk) {
if (widget.onError != null) {
widget.onError?.call(e, stk);
} else {
rethrow;
}
});
}
}
StreamSubscription? _keyboardListener;
void _showErrorAlert(String description) {
showModalBottomSheet(
backgroundColor: _streamChatTheme.colorTheme.white,
backgroundColor: _streamChatTheme.colorTheme.barsBg,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
@@ -2065,7 +2107,7 @@ class MessageInputState extends State<MessageInput> {
height: 26,
),
StreamSvgIcon.error(
color: _streamChatTheme.colorTheme.accentRed,
color: _streamChatTheme.colorTheme.accentError,
size: 24,
),
const SizedBox(
@@ -2089,7 +2131,8 @@ class MessageInputState extends State<MessageInput> {
height: 36,
),
Container(
color: _streamChatTheme.colorTheme.black.withOpacity(.08),
color:
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.08),
height: 1,
),
Row(
@@ -2101,8 +2144,8 @@ class MessageInputState extends State<MessageInput> {
},
child: Text(
context.translations.okLabel,
style: _streamChatTheme.textTheme.bodyBold
.copyWith(color: _streamChatTheme.colorTheme.accentBlue),
style: _streamChatTheme.textTheme.bodyBold.copyWith(
color: _streamChatTheme.colorTheme.accentPrimary),
),
),
],
@@ -2243,12 +2286,12 @@ class __PickerWidgetState extends State<_PickerWidget> {
},
child: Container(
constraints: const BoxConstraints.expand(),
color: widget.streamChatTheme.colorTheme.whiteSmoke,
color: widget.streamChatTheme.colorTheme.inputBg,
alignment: Alignment.center,
child: Text(
context.translations.addMoreFilesLabel,
style: TextStyle(
color: widget.streamChatTheme.colorTheme.accentBlue,
color: widget.streamChatTheme.colorTheme.accentPrimary,
fontWeight: FontWeight.bold,
),
),
@@ -2266,7 +2309,7 @@ class __PickerWidgetState extends State<_PickerWidget> {
PhotoManager.openSetting();
},
child: Container(
color: widget.streamChatTheme.colorTheme.whiteSmoke,
color: widget.streamChatTheme.colorTheme.inputBg,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -2275,12 +2318,13 @@ class __PickerWidgetState extends State<_PickerWidget> {
'svgs/icon_picture_empty_state.svg',
package: 'stream_chat_flutter',
height: 140,
color: widget.streamChatTheme.colorTheme.greyGainsboro,
color: widget.streamChatTheme.colorTheme.disabled,
),
Text(
context.translations.enablePhotoAndVideoAccessMessage,
style: widget.streamChatTheme.textTheme.body.copyWith(
color: widget.streamChatTheme.colorTheme.grey),
color:
widget.streamChatTheme.colorTheme.textLowEmphasis),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
@@ -2288,7 +2332,7 @@ class __PickerWidgetState extends State<_PickerWidget> {
child: Text(
context.translations.allowGalleryAccessMessage,
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
color: widget.streamChatTheme.colorTheme.accentBlue,
color: widget.streamChatTheme.colorTheme.accentPrimary,
),
),
),
@@ -143,7 +143,6 @@ class MessageListView extends StatefulWidget {
this.parentMessage,
this.threadBuilder,
this.onThreadTap,
this.onReplyTap,
this.dateDividerBuilder,
this.scrollPhysics = const ClampingScrollPhysics(),
this.initialScrollIndex,
@@ -153,7 +152,6 @@ class MessageListView extends StatefulWidget {
this.onMessageSwiped,
this.highlightInitialMessage = false,
this.messageHighlightColor,
this.onShowMessage,
this.showConnectionStateTile = false,
this.headerBuilder,
this.footerBuilder,
@@ -161,16 +159,11 @@ class MessageListView extends StatefulWidget {
this.emptyBuilder,
this.systemMessageBuilder,
this.messageListBuilder,
this.errorWidgetBuilder,
this.errorBuilder,
this.messageFilter,
this.customAttachmentBuilders,
this.onMessageTap,
this.onSystemMessageTap,
this.onAttachmentTap,
this.onLinkTap,
this.pinPermissions = const [],
this.textBuilder,
this.usernameBuilder,
this.showFloatingDateDivider = true,
this.threadSeparatorBuilder,
this.messageListController,
@@ -223,9 +216,6 @@ class MessageListView extends StatefulWidget {
/// Called when message item gets swiped
final OnMessageSwiped? onMessageSwiped;
///
final ReplyTapCallback? onReplyTap;
/// If true the list will highlight the initialMessage if there is any.
///
/// Also See [StreamChannel]
@@ -234,9 +224,6 @@ class MessageListView extends StatefulWidget {
/// Color used while highlighting initial message
final Color? messageHighlightColor;
/// Callback when show message is tapped
final ShowMessageCallback? onShowMessage;
/// Flag for showing tile on header
final bool showConnectionStateTile;
@@ -263,16 +250,11 @@ class MessageListView extends StatefulWidget {
/// This parameter can be used to display an error message to
/// users in the event
/// of a connection failure.
final ErrorBuilder? errorWidgetBuilder;
final ErrorBuilder? errorBuilder;
/// Predicate used to filter messages
final bool Function(Message)? messageFilter;
/// Attachment builders for the default message widget
/// Please change this in the [MessageWidget] if you are using a
/// custom implementation
final Map<String, AttachmentBuilder>? customAttachmentBuilders;
/// Called when any message is tapped except a system message
/// (use [onSystemMessageTap] instead)
final OnMessageTap? onMessageTap;
@@ -280,18 +262,6 @@ class MessageListView extends StatefulWidget {
/// Called when system message is tapped
final OnMessageTap? onSystemMessageTap;
/// Customize onTap on attachment
final void Function(Message message, Attachment attachment)? onAttachmentTap;
/// Customize the MessageWidget textBuilder
final Widget Function(BuildContext context, Message message)? textBuilder;
/// Customize the MessageWidget usernameBuilder
final Widget Function(BuildContext context, Message message)? usernameBuilder;
/// Callback for when link is tapped
final void Function(String link)? onLinkTap;
/// A List of user types that have permission to pin messages
final List<String> pinPermissions;
@@ -370,19 +340,21 @@ class _MessageListViewState extends State<MessageListView> {
child: Text(
context.translations.emptyChatMessagesText,
style: _streamTheme.textTheme.footnote.copyWith(
color: _streamTheme.colorTheme.black.withOpacity(.5)),
color: _streamTheme.colorTheme.textHighEmphasis
.withOpacity(.5)),
),
),
messageListBuilder: widget.messageListBuilder ??
(context, list) => _buildListView(list),
messageListController: _messageListController,
parentMessage: widget.parentMessage,
errorWidgetBuilder: widget.errorWidgetBuilder ??
errorBuilder: widget.errorBuilder ??
(BuildContext context, Object error) => Center(
child: Text(
context.translations.genericErrorText,
style: _streamTheme.textTheme.footnote.copyWith(
color: _streamTheme.colorTheme.black.withOpacity(.5)),
color: _streamTheme.colorTheme.textHighEmphasis
.withOpacity(.5)),
),
),
);
@@ -712,7 +684,7 @@ class _MessageListViewState extends State<MessageListView> {
clipBehavior: Clip.none,
children: [
FloatingActionButton(
backgroundColor: _streamTheme.colorTheme.white,
backgroundColor: _streamTheme.colorTheme.barsBg,
onPressed: () {
if (unreadCount > 0) {
streamChannel!.channel.markRead();
@@ -731,7 +703,7 @@ class _MessageListViewState extends State<MessageListView> {
}
},
child: StreamSvgIcon.down(
color: _streamTheme.colorTheme.black,
color: _streamTheme.colorTheme.textHighEmphasis,
),
),
if (showUnreadCount)
@@ -823,7 +795,6 @@ class _MessageListViewState extends State<MessageListView> {
showUsername: !isMyMessage,
padding: const EdgeInsets.all(8),
showSendingIndicator: false,
onThreadTap: _onThreadTap,
borderRadiusGeometry: BorderRadius.only(
topLeft: const Radius.circular(16),
bottomLeft:
@@ -841,7 +812,6 @@ class _MessageListViewState extends State<MessageListView> {
messageTheme: isMyMessage
? _streamTheme.ownMessageTheme
: _streamTheme.otherMessageTheme,
onShowMessage: widget.onShowMessage,
onReturnAction: (action) {
switch (action) {
case ReturnActionType.none:
@@ -852,16 +822,12 @@ class _MessageListViewState extends State<MessageListView> {
break;
}
},
customAttachmentBuilders: widget.customAttachmentBuilders,
onMessageTap: (message) {
if (widget.onMessageTap != null) {
widget.onMessageTap!(message);
}
FocusScope.of(context).unfocus();
},
textBuilder: widget.textBuilder,
usernameBuilder: widget.usernameBuilder,
onLinkTap: widget.onLinkTap,
showPinButton: currentUserMember != null &&
widget.pinPermissions.contains(currentUserMember.role),
);
@@ -899,7 +865,7 @@ class _MessageListViewState extends State<MessageListView> {
final userId = StreamChat.of(context).user!.id;
final isMyMessage = message.user!.id == userId;
final nextMessage = index - 3 >= 0 ? messages[index - 3] : null;
final nextMessage = index - 1 >= 0 ? messages[index - 1] : null;
final isNextUserSame =
nextMessage != null && message.user!.id == nextMessage.user!.id;
@@ -1004,7 +970,6 @@ class _MessageListViewState extends State<MessageListView> {
showFlagButton: !isMyMessage,
borderSide: borderSide,
onThreadTap: _onThreadTap,
onReplyTap: widget.onReplyTap,
attachmentBorderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(attachmentBorderRadius),
bottomLeft: isMyMessage
@@ -1055,7 +1020,6 @@ class _MessageListViewState extends State<MessageListView> {
: _streamTheme.otherMessageTheme,
readList: readList,
allRead: allRead,
onShowMessage: widget.onShowMessage,
onReturnAction: (action) {
switch (action) {
case ReturnActionType.none:
@@ -1066,17 +1030,12 @@ class _MessageListViewState extends State<MessageListView> {
break;
}
},
customAttachmentBuilders: widget.customAttachmentBuilders,
onMessageTap: (message) {
if (widget.onMessageTap != null) {
widget.onMessageTap!(message);
}
FocusScope.of(context).unfocus();
},
onAttachmentTap: widget.onAttachmentTap,
textBuilder: widget.textBuilder,
usernameBuilder: widget.usernameBuilder,
onLinkTap: widget.onLinkTap,
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
);
@@ -1108,7 +1067,7 @@ class _MessageListViewState extends State<MessageListView> {
widget.onMessageSwiped?.call(message);
},
backgroundIcon: StreamSvgIcon.reply(
color: _streamTheme.colorTheme.accentBlue,
color: _streamTheme.colorTheme.accentPrimary,
),
child: child,
),
@@ -1124,7 +1083,7 @@ class _MessageListViewState extends State<MessageListView> {
child = TweenAnimationBuilder<Color?>(
tween: ColorTween(
begin: highlightColor,
end: colorTheme.white.withOpacity(0),
end: colorTheme.barsBg.withOpacity(0),
),
duration: const Duration(seconds: 3),
onEnd: () => initialMessageHighlightComplete = true,
@@ -1254,7 +1213,7 @@ class _LoadingIndicator extends StatelessWidget {
stream: stream,
initialData: false,
errorBuilder: (context, error) => Container(
color: streamTheme.colorTheme.accentRed.withOpacity(.2),
color: streamTheme.colorTheme.accentError.withOpacity(.2),
child: Center(
child: Text(context.translations.loadingMessagesError),
),
@@ -143,7 +143,7 @@ class MessageReactionsModal extends StatelessWidget {
Widget _buildReactionCard(BuildContext context, User? user) {
final chatThemeData = StreamChatTheme.of(context);
return Card(
color: chatThemeData.colorTheme.white,
color: chatThemeData.colorTheme.barsBg,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@@ -225,7 +225,7 @@ class MessageReactionsModal extends StatelessWidget {
messageTheme.reactionsBorderColor ?? Colors.transparent,
backgroundColor: messageTheme.reactionsBackgroundColor ??
Colors.transparent,
maskColor: chatThemeData.colorTheme.white,
maskColor: chatThemeData.colorTheme.barsBg,
tailCirclesSpacing: 1,
highlightOwnReactions: false,
),
@@ -201,7 +201,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
Widget _separatorBuilder(BuildContext context, int index) => Container(
height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
color: StreamChatTheme.of(context).colorTheme.borders,
);
Widget _listItemBuilder(
@@ -226,7 +226,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.accentError
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -296,7 +296,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
child: Text(
context.translations.resultCountText(items.length),
style: TextStyle(
color: chatThemeData.colorTheme.grey,
color: chatThemeData.colorTheme.textLowEmphasis,
),
),
),
@@ -223,7 +223,7 @@ class MessageWidget extends StatefulWidget {
final border = RoundedRectangleBorder(
side: attachmentBorderSide ??
BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
color: StreamChatTheme.of(context).colorTheme.borders,
),
borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
);
@@ -750,7 +750,7 @@ class _MessageWidgetState extends State<MessageWidget>
child: CustomPaint(
painter: ReactionBubblePainter(
_streamChatTheme
.colorTheme.white,
.colorTheme.barsBg,
Colors.transparent,
Colors.transparent,
tailCirclesSpace: 1,
@@ -836,14 +836,14 @@ class _MessageWidgetState extends State<MessageWidget>
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: chatThemeData.colorTheme.grey,
color: chatThemeData.colorTheme.textLowEmphasis,
size: 16,
),
const SizedBox(width: 8),
Text(
context.translations.onlyVisibleToYouText,
style: chatThemeData.textTheme.footnote
.copyWith(color: chatThemeData.colorTheme.grey),
.copyWith(color: chatThemeData.colorTheme.textLowEmphasis),
),
],
);
@@ -1222,7 +1222,7 @@ class _MessageWidgetState extends State<MessageWidget>
Text(
widget.readList!.length.toString(),
style: style.copyWith(
color: _streamChatTheme.colorTheme.accentBlue,
color: _streamChatTheme.colorTheme.accentPrimary,
),
),
const SizedBox(width: 2),
@@ -1299,7 +1299,7 @@ class _MessageWidgetState extends State<MessageWidget>
currentUser: currentUser,
),
style: TextStyle(
color: _streamChatTheme.colorTheme.grey,
color: _streamChatTheme.colorTheme.textLowEmphasis,
fontSize: 13,
fontWeight: FontWeight.w400,
),
@@ -1317,7 +1317,7 @@ class _MessageWidgetState extends State<MessageWidget>
}
if (hasUrlAttachments) {
return _streamChatTheme.colorTheme.blueAlice;
return _streamChatTheme.colorTheme.linkBg;
}
if (isOnlyEmoji) {
@@ -1373,7 +1373,7 @@ class _ThreadParticipants extends StatelessWidget {
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _streamChatTheme.colorTheme.white,
color: _streamChatTheme.colorTheme.barsBg,
),
padding: const EdgeInsets.all(1),
child: UserAvatar(
@@ -1402,7 +1402,7 @@ class _ThreadReplyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color ?? StreamChatTheme.of(context!).colorTheme.greyGainsboro
..color = color ?? StreamChatTheme.of(context!).colorTheme.disabled
..style = PaintingStyle.stroke
..strokeWidth = 1
..strokeCap = StrokeCap.round;
@@ -46,11 +46,11 @@ class OptionListTile extends StatelessWidget {
return Column(
children: [
Container(
color: separatorColor ?? chatThemeData.colorTheme.greyGainsboro,
color: separatorColor ?? chatThemeData.colorTheme.disabled,
height: 1,
),
Material(
color: tileColor ?? chatThemeData.colorTheme.white,
color: tileColor ?? chatThemeData.colorTheme.barsBg,
child: SizedBox(
height: 63,
child: InkWell(
@@ -154,7 +154,7 @@ class QuotedMessageWidget extends StatelessWidget {
color: _getBackgroundColor(context),
border: showBorder
? Border.all(
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
color: StreamChatTheme.of(context).colorTheme.disabled,
)
: null,
borderRadius: BorderRadius.only(
@@ -280,7 +280,7 @@ class QuotedMessageWidget extends StatelessWidget {
Color? _getBackgroundColor(BuildContext context) {
if (_containsScrapeUrl) {
return StreamChatTheme.of(context).colorTheme.blueAlice;
return StreamChatTheme.of(context).colorTheme.linkBg;
}
return messageTheme.messageBackgroundColor;
}
@@ -141,8 +141,8 @@ class ReactionBubble extends StatelessWidget {
Icons.help_outline_rounded,
size: 16,
color: (!highlightOwnReactions || reaction.user?.id == userId)
? chatThemeData.colorTheme.accentBlue
: chatThemeData.colorTheme.black.withOpacity(.5),
? chatThemeData.colorTheme.accentPrimary
: chatThemeData.colorTheme.textHighEmphasis.withOpacity(.5),
),
);
}
@@ -49,7 +49,7 @@ class _ReactionPickerState extends State<ReactionPicker>
final child = Material(
borderRadius: BorderRadius.circular(24),
color: chatThemeData.colorTheme.white,
color: chatThemeData.colorTheme.barsBg,
clipBehavior: Clip.hardEdge,
child: Padding(
padding: const EdgeInsets.symmetric(
@@ -25,7 +25,7 @@ class SendingIndicator extends StatelessWidget {
if (isMessageRead) {
return StreamSvgIcon.checkAll(
size: size,
color: StreamChatTheme.of(context).colorTheme.accentBlue,
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
);
}
if (message.status == MessageSendingStatus.sent) {
@@ -101,8 +101,8 @@ class StreamChatState extends State<StreamChat> {
return Theme(
data: materialTheme.copyWith(
primaryIconTheme: streamTheme.primaryIconTheme,
accentColor: streamTheme.colorTheme.accentBlue,
scaffoldBackgroundColor: streamTheme.colorTheme.white,
accentColor: streamTheme.colorTheme.accentPrimary,
scaffoldBackgroundColor: streamTheme.colorTheme.barsBg,
),
child: StreamChatCore(
client: client,
@@ -1,4 +1,5 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/channel_header.dart';
import 'package:stream_chat_flutter/src/channel_preview.dart';
@@ -54,10 +55,11 @@ class StreamChatThemeData {
MessageTheme? otherMessageTheme,
MessageTheme? ownMessageTheme,
MessageInputTheme? messageInputTheme,
Widget Function(BuildContext, Channel)? defaultChannelImage,
Widget Function(BuildContext, User)? defaultUserImage,
IconThemeData? primaryIconTheme,
List<ReactionIcon>? reactionIcons,
GalleryHeaderThemeData? imageHeaderTheme,
GalleryFooterThemeData? imageFooterTheme,
}) {
brightness ??= colorTheme?.brightness ?? Brightness.light;
final isDark = brightness == Brightness.dark;
@@ -76,10 +78,11 @@ class StreamChatThemeData {
otherMessageTheme: otherMessageTheme,
ownMessageTheme: ownMessageTheme,
messageInputTheme: messageInputTheme,
defaultChannelImage: defaultChannelImage,
defaultUserImage: defaultUserImage,
primaryIconTheme: primaryIconTheme,
reactionIcons: reactionIcons,
galleryHeaderTheme: imageHeaderTheme,
galleryFooterTheme: imageFooterTheme,
);
return defaultData.merge(customizedData);
@@ -103,10 +106,11 @@ class StreamChatThemeData {
required this.otherMessageTheme,
required this.ownMessageTheme,
required this.messageInputTheme,
required this.defaultChannelImage,
required this.defaultUserImage,
required this.primaryIconTheme,
required this.reactionIcons,
required this.galleryHeaderTheme,
required this.galleryFooterTheme,
});
/// Create a theme from a Material [Theme]
@@ -114,7 +118,7 @@ class StreamChatThemeData {
final defaultTheme = StreamChatThemeData(brightness: theme.brightness);
final customizedTheme = StreamChatThemeData.fromColorAndTextTheme(
defaultTheme.colorTheme.copyWith(
accentBlue: theme.accentColor,
accentPrimary: theme.accentColor,
),
defaultTheme.textTheme,
);
@@ -136,6 +140,14 @@ class StreamChatThemeData {
/// Theme of the chat widgets dedicated to a channel
final ChannelTheme channelTheme;
/// The default style for [GalleryHeader]s below the overall
/// [StreamChatTheme].
final GalleryHeaderThemeData galleryHeaderTheme;
/// The default style for [GalleryFooter]s below the overall
/// [StreamChatTheme].
final GalleryFooterThemeData galleryFooterTheme;
/// Theme of the current user messages
final MessageTheme ownMessageTheme;
@@ -145,9 +157,6 @@ class StreamChatThemeData {
/// Theme dedicated to the [MessageInput] widget
final MessageInputTheme messageInputTheme;
/// The widget that will be built when the channel image is unavailable
final Widget Function(BuildContext, Channel) defaultChannelImage;
/// The widget that will be built when the user image is unavailable
final Widget Function(BuildContext, User) defaultUserImage;
@@ -167,11 +176,12 @@ class StreamChatThemeData {
MessageTheme? ownMessageTheme,
MessageTheme? otherMessageTheme,
MessageInputTheme? messageInputTheme,
Widget Function(BuildContext, Channel)? defaultChannelImage,
Widget Function(BuildContext, User)? defaultUserImage,
IconThemeData? primaryIconTheme,
ChannelListHeaderTheme? channelListHeaderTheme,
List<ReactionIcon>? reactionIcons,
GalleryHeaderThemeData? galleryHeaderTheme,
GalleryFooterThemeData? galleryFooterTheme,
}) =>
StreamChatThemeData.raw(
channelListHeaderTheme:
@@ -179,7 +189,6 @@ class StreamChatThemeData {
textTheme: this.textTheme.merge(textTheme),
colorTheme: this.colorTheme.merge(colorTheme),
primaryIconTheme: this.primaryIconTheme.merge(primaryIconTheme),
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
defaultUserImage: defaultUserImage ?? this.defaultUserImage,
channelPreviewTheme:
this.channelPreviewTheme.merge(channelPreviewTheme),
@@ -188,6 +197,8 @@ class StreamChatThemeData {
otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme),
messageInputTheme: this.messageInputTheme.merge(messageInputTheme),
reactionIcons: reactionIcons ?? this.reactionIcons,
galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme,
galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme,
);
/// Merge themes
@@ -199,7 +210,6 @@ class StreamChatThemeData {
textTheme: textTheme.merge(other.textTheme),
colorTheme: colorTheme.merge(other.colorTheme),
primaryIconTheme: other.primaryIconTheme,
defaultChannelImage: other.defaultChannelImage,
defaultUserImage: other.defaultUserImage,
channelPreviewTheme: channelPreviewTheme.merge(other.channelPreviewTheme),
channelTheme: channelTheme.merge(other.channelTheme),
@@ -207,6 +217,8 @@ class StreamChatThemeData {
otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme),
messageInputTheme: messageInputTheme.merge(other.messageInputTheme),
reactionIcons: other.reactionIcons,
galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme),
galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme),
);
}
@@ -216,22 +228,11 @@ class StreamChatThemeData {
ColorTheme colorTheme,
TextTheme textTheme,
) {
final accentColor = colorTheme.accentBlue;
final iconTheme = IconThemeData(color: colorTheme.black.withOpacity(.5));
return StreamChatThemeData.raw(
textTheme: textTheme,
colorTheme: colorTheme,
primaryIconTheme: iconTheme,
defaultChannelImage: (context, channel) => const SizedBox(),
defaultUserImage: (context, user) => Center(
child: CachedNetworkImage(
filterQuality: FilterQuality.high,
imageUrl: getRandomPicUrl(user),
fit: BoxFit.cover,
),
),
channelPreviewTheme: ChannelPreviewTheme(
unreadCounterColor: colorTheme.accentRed,
final accentColor = colorTheme.accentPrimary;
final iconTheme =
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5));
final channelTheme = ChannelTheme(
channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
@@ -239,15 +240,43 @@ class StreamChatThemeData {
width: 40,
),
),
title: textTheme.bodyBold,
color: colorTheme.barsBg,
title: textTheme.headlineBold,
subtitle: textTheme.footnote.copyWith(
color: const Color(0xff7A7A7A),
),
lastMessageAt: textTheme.footnote.copyWith(
color: colorTheme.black.withOpacity(.5),
),
indicatorIconSize: 16,
),
);
final channelPreviewTheme = ChannelPreviewTheme(
unreadCounterColor: colorTheme.accentError,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
title: textTheme.bodyBold,
subtitle: textTheme.footnote.copyWith(
color: const Color(0xff7A7A7A),
),
lastMessageAt: textTheme.footnote.copyWith(
color: colorTheme.textHighEmphasis.withOpacity(.5),
),
indicatorIconSize: 16,
);
return StreamChatThemeData.raw(
textTheme: textTheme,
colorTheme: colorTheme,
primaryIconTheme: iconTheme,
defaultUserImage: (context, user) => Center(
child: CachedNetworkImage(
filterQuality: FilterQuality.high,
imageUrl: getRandomPicUrl(user),
fit: BoxFit.cover,
),
),
channelPreviewTheme: channelPreviewTheme,
channelListHeaderTheme: ChannelListHeaderTheme(
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
@@ -256,35 +285,22 @@ class StreamChatThemeData {
width: 40,
),
),
color: colorTheme.white,
color: colorTheme.barsBg,
title: textTheme.headlineBold,
),
channelTheme: ChannelTheme(
channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
color: colorTheme.white,
title: textTheme.headlineBold,
subtitle: textTheme.footnote.copyWith(
color: const Color(0xff7A7A7A),
),
),
),
channelTheme: channelTheme,
ownMessageTheme: MessageTheme(
messageAuthor: textTheme.footnote.copyWith(color: colorTheme.grey),
messageAuthor:
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
messageText: textTheme.body,
createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
createdAt:
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
replies: textTheme.footnoteBold.copyWith(color: accentColor),
messageBackgroundColor: colorTheme.greyGainsboro,
reactionsBackgroundColor: colorTheme.white,
reactionsBorderColor: colorTheme.greyWhisper,
reactionsMaskColor: colorTheme.whiteSnow,
messageBorderColor: colorTheme.greyGainsboro,
messageBackgroundColor: colorTheme.disabled,
reactionsBackgroundColor: colorTheme.barsBg,
reactionsBorderColor: colorTheme.borders,
reactionsMaskColor: colorTheme.appBg,
messageBorderColor: colorTheme.disabled,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
@@ -297,18 +313,20 @@ class StreamChatThemeData {
),
),
otherMessageTheme: MessageTheme(
reactionsBackgroundColor: colorTheme.greyGainsboro,
reactionsBorderColor: colorTheme.white,
reactionsMaskColor: colorTheme.whiteSnow,
reactionsBackgroundColor: colorTheme.disabled,
reactionsBorderColor: colorTheme.barsBg,
reactionsMaskColor: colorTheme.appBg,
messageText: textTheme.body,
createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
messageAuthor: textTheme.footnote.copyWith(color: colorTheme.grey),
createdAt:
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
messageAuthor:
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
replies: textTheme.footnoteBold.copyWith(color: accentColor),
messageLinks: TextStyle(
color: accentColor,
),
messageBackgroundColor: colorTheme.white,
messageBorderColor: colorTheme.greyWhisper,
messageBackgroundColor: colorTheme.barsBg,
messageBorderColor: colorTheme.borders,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
@@ -320,23 +338,23 @@ class StreamChatThemeData {
messageInputTheme: MessageInputTheme(
borderRadius: BorderRadius.circular(20),
sendAnimationDuration: const Duration(milliseconds: 300),
actionButtonColor: colorTheme.accentBlue,
actionButtonIdleColor: colorTheme.grey,
expandButtonColor: colorTheme.accentBlue,
sendButtonColor: colorTheme.accentBlue,
sendButtonIdleColor: colorTheme.greyGainsboro,
inputBackground: colorTheme.white,
actionButtonColor: colorTheme.accentPrimary,
actionButtonIdleColor: colorTheme.textLowEmphasis,
expandButtonColor: colorTheme.accentPrimary,
sendButtonColor: colorTheme.accentPrimary,
sendButtonIdleColor: colorTheme.disabled,
inputBackground: colorTheme.barsBg,
inputTextStyle: textTheme.body,
idleBorderGradient: LinearGradient(
colors: [
colorTheme.greyGainsboro,
colorTheme.greyGainsboro,
colorTheme.disabled,
colorTheme.disabled,
],
),
activeBorderGradient: LinearGradient(
colors: [
colorTheme.greyGainsboro,
colorTheme.greyGainsboro,
colorTheme.disabled,
colorTheme.disabled,
],
),
),
@@ -347,7 +365,7 @@ class StreamChatThemeData {
final theme = StreamChatTheme.of(context);
return StreamSvgIcon.loveReaction(
color: highlighted
? theme.colorTheme.accentBlue
? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5),
size: size,
);
@@ -359,7 +377,7 @@ class StreamChatThemeData {
final theme = StreamChatTheme.of(context);
return StreamSvgIcon.thumbsUpReaction(
color: highlighted
? theme.colorTheme.accentBlue
? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5),
size: size,
);
@@ -371,7 +389,7 @@ class StreamChatThemeData {
final theme = StreamChatTheme.of(context);
return StreamSvgIcon.thumbsDownReaction(
color: highlighted
? theme.colorTheme.accentBlue
? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5),
size: size,
);
@@ -383,7 +401,7 @@ class StreamChatThemeData {
final theme = StreamChatTheme.of(context);
return StreamSvgIcon.lolReaction(
color: highlighted
? theme.colorTheme.accentBlue
? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5),
size: size,
);
@@ -395,13 +413,31 @@ class StreamChatThemeData {
final theme = StreamChatTheme.of(context);
return StreamSvgIcon.wutReaction(
color: highlighted
? theme.colorTheme.accentBlue
? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5),
size: size,
);
},
),
],
galleryHeaderTheme: GalleryHeaderThemeData(
closeButtonColor: colorTheme.textHighEmphasis,
backgroundColor: channelTheme.channelHeaderTheme.color,
iconMenuPointColor: colorTheme.textHighEmphasis,
titleTextStyle: textTheme.headlineBold,
subtitleTextStyle: channelPreviewTheme.subtitle,
bottomSheetBarrierColor: colorTheme.overlay,
),
galleryFooterTheme: GalleryFooterThemeData(
backgroundColor: colorTheme.barsBg,
shareIconColor: colorTheme.textHighEmphasis,
titleTextStyle: textTheme.headlineBold,
gridIconButtonColor: colorTheme.textHighEmphasis,
bottomSheetBarrierColor: colorTheme.overlay,
bottomSheetBackgroundColor: colorTheme.barsBg,
bottomSheetPhotosTextStyle: textTheme.headlineBold,
bottomSheetCloseIconColor: colorTheme.textHighEmphasis,
),
);
}
}
@@ -572,17 +608,17 @@ class TextTheme {
class ColorTheme {
/// Initialise with light theme
ColorTheme.light({
this.black = const Color(0xff000000),
this.grey = const Color(0xff7a7a7a),
this.greyGainsboro = const Color(0xffdbdbdb),
this.greyWhisper = const Color(0xffecebeb),
this.whiteSmoke = const Color(0xfff2f2f2),
this.whiteSnow = const Color(0xfffcfcfc),
this.white = const Color(0xffffffff),
this.blueAlice = const Color(0xffe9f2ff),
this.accentBlue = const Color(0xff005FFF),
this.accentRed = const Color(0xffFF3842),
this.accentGreen = const Color(0xff20E070),
this.textHighEmphasis = const Color(0xff000000),
this.textLowEmphasis = const Color(0xff7a7a7a),
this.disabled = const Color(0xffdbdbdb),
this.borders = const Color(0xffecebeb),
this.inputBg = const Color(0xfff2f2f2),
this.appBg = const Color(0xfffcfcfc),
this.barsBg = const Color(0xffffffff),
this.linkBg = const Color(0xffe9f2ff),
this.accentPrimary = const Color(0xff005FFF),
this.accentError = const Color(0xffFF3842),
this.accentInfo = const Color(0xff20E070),
this.highlight = const Color(0xfffbf4dd),
this.overlay = const Color.fromRGBO(0, 0, 0, 0.2),
this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6),
@@ -604,17 +640,17 @@ class ColorTheme {
/// Initialise with dark theme
ColorTheme.dark({
this.black = const Color(0xffffffff),
this.grey = const Color(0xff7a7a7a),
this.greyGainsboro = const Color(0xff2d2f2f),
this.greyWhisper = const Color(0xff1c1e22),
this.whiteSmoke = const Color(0xff13151b),
this.whiteSnow = const Color(0xff070A0D),
this.white = const Color(0xff101418),
this.blueAlice = const Color(0xff00193D),
this.accentBlue = const Color(0xff005FFF),
this.accentRed = const Color(0xffFF3742),
this.accentGreen = const Color(0xff20E070),
this.textHighEmphasis = const Color(0xffffffff),
this.textLowEmphasis = const Color(0xff7a7a7a),
this.disabled = const Color(0xff2d2f2f),
this.borders = const Color(0xff1c1e22),
this.inputBg = const Color(0xff13151b),
this.appBg = const Color(0xff070A0D),
this.barsBg = const Color(0xff101418),
this.linkBg = const Color(0xff00193D),
this.accentPrimary = const Color(0xff005FFF),
this.accentError = const Color(0xffFF3742),
this.accentInfo = const Color(0xff20E070),
this.borderTop = const Effect(
sigmaX: 0,
sigmaY: -1,
@@ -657,37 +693,37 @@ class ColorTheme {
}) : brightness = Brightness.dark;
///
final Color black;
final Color textHighEmphasis;
///
final Color grey;
final Color textLowEmphasis;
///
final Color greyGainsboro;
final Color disabled;
///
final Color greyWhisper;
final Color borders;
///
final Color whiteSmoke;
final Color inputBg;
///
final Color whiteSnow;
final Color appBg;
///
final Color white;
final Color barsBg;
///
final Color blueAlice;
final Color linkBg;
///
final Color accentBlue;
final Color accentPrimary;
///
final Color accentRed;
final Color accentError;
///
final Color accentGreen;
final Color accentInfo;
///
final Effect borderTop;
@@ -719,17 +755,17 @@ class ColorTheme {
/// Copy with theme
ColorTheme copyWith({
Brightness brightness = Brightness.light,
Color? black,
Color? grey,
Color? greyGainsboro,
Color? greyWhisper,
Color? whiteSmoke,
Color? whiteSnow,
Color? white,
Color? blueAlice,
Color? accentBlue,
Color? accentRed,
Color? accentGreen,
Color? textHighEmphasis,
Color? textLowEmphasis,
Color? disabled,
Color? borders,
Color? inputBg,
Color? appBg,
Color? barsBg,
Color? linkBg,
Color? accentPrimary,
Color? accentError,
Color? accentInfo,
Effect? borderTop,
Effect? borderBottom,
Effect? shadowIconButton,
@@ -741,17 +777,17 @@ class ColorTheme {
}) =>
brightness == Brightness.light
? ColorTheme.light(
black: black ?? this.black,
grey: grey ?? this.grey,
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
greyWhisper: greyWhisper ?? this.greyWhisper,
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
whiteSnow: whiteSnow ?? this.whiteSnow,
white: white ?? this.white,
blueAlice: blueAlice ?? this.blueAlice,
accentBlue: accentBlue ?? this.accentBlue,
accentRed: accentRed ?? this.accentRed,
accentGreen: accentGreen ?? this.accentGreen,
textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis,
textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis,
disabled: disabled ?? this.disabled,
borders: borders ?? this.borders,
inputBg: inputBg ?? this.inputBg,
appBg: appBg ?? this.appBg,
barsBg: barsBg ?? this.barsBg,
linkBg: linkBg ?? this.linkBg,
accentPrimary: accentPrimary ?? this.accentPrimary,
accentError: accentError ?? this.accentError,
accentInfo: accentInfo ?? this.accentInfo,
borderTop: borderTop ?? this.borderTop,
borderBottom: borderBottom ?? this.borderBottom,
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
@@ -762,17 +798,17 @@ class ColorTheme {
bgGradient: bgGradient ?? this.bgGradient,
)
: ColorTheme.dark(
black: black ?? this.black,
grey: grey ?? this.grey,
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
greyWhisper: greyWhisper ?? this.greyWhisper,
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
whiteSnow: whiteSnow ?? this.whiteSnow,
white: white ?? this.white,
blueAlice: blueAlice ?? this.blueAlice,
accentBlue: accentBlue ?? this.accentBlue,
accentRed: accentRed ?? this.accentRed,
accentGreen: accentGreen ?? this.accentGreen,
textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis,
textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis,
disabled: disabled ?? this.disabled,
borders: borders ?? this.borders,
inputBg: inputBg ?? this.inputBg,
appBg: appBg ?? this.appBg,
barsBg: barsBg ?? this.barsBg,
linkBg: linkBg ?? this.linkBg,
accentPrimary: accentPrimary ?? this.accentPrimary,
accentError: accentError ?? this.accentError,
accentInfo: accentInfo ?? this.accentInfo,
borderTop: borderTop ?? this.borderTop,
borderBottom: borderBottom ?? this.borderBottom,
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
@@ -787,17 +823,17 @@ class ColorTheme {
ColorTheme merge(ColorTheme? other) {
if (other == null) return this;
return copyWith(
black: other.black,
grey: other.grey,
greyGainsboro: other.greyGainsboro,
greyWhisper: other.greyWhisper,
whiteSmoke: other.whiteSmoke,
whiteSnow: other.whiteSnow,
white: other.white,
blueAlice: other.blueAlice,
accentBlue: other.accentBlue,
accentRed: other.accentRed,
accentGreen: other.accentGreen,
textHighEmphasis: other.textHighEmphasis,
textLowEmphasis: other.textLowEmphasis,
disabled: other.disabled,
borders: other.borders,
inputBg: other.inputBg,
appBg: other.appBg,
barsBg: other.barsBg,
linkBg: other.linkBg,
accentPrimary: other.accentPrimary,
accentError: other.accentError,
accentInfo: other.accentInfo,
highlight: other.highlight,
overlay: other.overlay,
overlayDark: other.overlayDark,
@@ -1282,3 +1318,393 @@ class Effect {
blur: blur ?? this.blur,
);
}
/// Overrides the default style of [GalleryHeader] descendants.
///
/// See also:
///
/// * [GalleryHeaderThemeData], which is used to configure this theme.
class GalleryHeaderTheme extends InheritedTheme {
/// Creates an [GalleryHeaderTheme].
///
/// The [data] parameter must not be null.
const GalleryHeaderTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final GalleryHeaderThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [GalleryHeaderTheme] widget, then
/// [StreamChatThemeData.galleryHeaderTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// ImageHeaderTheme theme = ImageHeaderTheme.of(context);
/// ```
static GalleryHeaderThemeData of(BuildContext context) {
final galleryHeaderTheme =
context.dependOnInheritedWidgetOfExactType<GalleryHeaderTheme>();
return galleryHeaderTheme?.data ??
StreamChatTheme.of(context).galleryHeaderTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
GalleryHeaderTheme(data: data, child: child);
@override
bool updateShouldNotify(GalleryHeaderTheme oldWidget) =>
data != oldWidget.data;
}
/// A style that overrides the default appearance of [GalleryHeader]s when used
/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.galleryHeaderTheme].
///
/// See also:
///
/// * [GalleryHeaderTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.galleryHeaderTheme], which can be used to override
/// the default style for [GalleryHeader]s below the overall [StreamChatTheme].
class GalleryHeaderThemeData with Diagnosticable {
/// Creates an [GalleryHeaderThemeData].
const GalleryHeaderThemeData({
this.closeButtonColor,
this.backgroundColor,
this.iconMenuPointColor,
this.titleTextStyle,
this.subtitleTextStyle,
this.bottomSheetBarrierColor,
});
/// The color of the "close" button.
///
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? closeButtonColor;
/// The background color of the [GalleryHeader] widget.
///
/// Defaults to [ChannelHeaderTheme.color].
final Color? backgroundColor;
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? iconMenuPointColor;
/// The [TextStyle] to use for the [GalleryHeader] title text.
///
/// Defaults to [TextTheme.headlineBold].
final TextStyle? titleTextStyle;
/// The [TextStyle] to use for the [GalleryHeader] subtitle text.
///
/// Defaults to [ChannelPreviewTheme.subtitle].
final TextStyle? subtitleTextStyle;
///
final Color? bottomSheetBarrierColor;
/// Copies this [GalleryHeaderThemeData] to another.
GalleryHeaderThemeData copyWith({
Color? closeButtonColor,
Color? backgroundColor,
Color? iconMenuPointColor,
TextStyle? titleTextStyle,
TextStyle? subtitleTextStyle,
Color? bottomSheetBarrierColor,
}) =>
GalleryHeaderThemeData(
closeButtonColor: closeButtonColor ?? this.closeButtonColor,
backgroundColor: backgroundColor ?? this.backgroundColor,
iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor,
titleTextStyle: titleTextStyle ?? this.titleTextStyle,
subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle,
bottomSheetBarrierColor:
bottomSheetBarrierColor ?? this.bottomSheetBarrierColor,
);
/// Linearly interpolate between two [GalleryHeader] themes.
///
/// All the properties must be non-null.
GalleryHeaderThemeData lerp(
GalleryHeaderThemeData a,
GalleryHeaderThemeData b,
double t,
) =>
GalleryHeaderThemeData(
closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t),
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
iconMenuPointColor:
Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t),
titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t),
subtitleTextStyle:
TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t),
bottomSheetBarrierColor:
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
);
/// Merges one [GalleryHeaderThemeData] with the another
GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) {
if (other == null) return this;
return copyWith(
closeButtonColor: other.closeButtonColor,
backgroundColor: other.backgroundColor,
iconMenuPointColor: other.iconMenuPointColor,
titleTextStyle: other.titleTextStyle,
subtitleTextStyle: other.subtitleTextStyle,
bottomSheetBarrierColor: other.bottomSheetBarrierColor,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GalleryHeaderThemeData &&
runtimeType == other.runtimeType &&
closeButtonColor == other.closeButtonColor &&
backgroundColor == other.backgroundColor &&
iconMenuPointColor == other.iconMenuPointColor &&
titleTextStyle == other.titleTextStyle &&
subtitleTextStyle == other.subtitleTextStyle &&
bottomSheetBarrierColor == other.bottomSheetBarrierColor;
@override
int get hashCode =>
closeButtonColor.hashCode ^
backgroundColor.hashCode ^
iconMenuPointColor.hashCode ^
titleTextStyle.hashCode ^
subtitleTextStyle.hashCode ^
bottomSheetBarrierColor.hashCode;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ColorProperty('closeButtonColor', closeButtonColor))
..add(ColorProperty('backgroundColor', backgroundColor))
..add(ColorProperty('iconMenuPointColor', iconMenuPointColor))
..add(DiagnosticsProperty('titleTextStyle', titleTextStyle))
..add(DiagnosticsProperty('subtitleTextStyle', subtitleTextStyle))
..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor));
}
}
/// Overrides the default style of [GalleryFooter] descendants.
///
/// See also:
///
/// * [GalleryFooterThemeData], which is used to configure this theme.
class GalleryFooterTheme extends InheritedTheme {
/// Creates an [GalleryFooterTheme].
///
/// The [data] parameter must not be null.
const GalleryFooterTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final GalleryFooterThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [GalleryFooterTheme] widget, then
/// [StreamChatThemeData.galleryFooterTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// ImageFooterTheme theme = ImageFooterTheme.of(context);
/// ```
static GalleryFooterThemeData of(BuildContext context) {
final imageFooterTheme =
context.dependOnInheritedWidgetOfExactType<GalleryFooterTheme>();
return imageFooterTheme?.data ??
StreamChatTheme.of(context).galleryFooterTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
GalleryFooterTheme(data: data, child: child);
@override
bool updateShouldNotify(GalleryFooterTheme oldWidget) =>
data != oldWidget.data;
}
/// A style that overrides the default appearance of [GalleryFooter]s when used
/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.galleryFooterTheme].
///
/// See also:
///
/// * [GalleryFooterTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.galleryFooterTheme], which can be used to override
/// the default style for [GalleryFooter]s below the overall [StreamChatTheme].
class GalleryFooterThemeData with Diagnosticable {
/// Creates an [GalleryFooterThemeData].
const GalleryFooterThemeData({
this.backgroundColor,
this.shareIconColor,
this.titleTextStyle,
this.gridIconButtonColor,
this.bottomSheetBarrierColor,
this.bottomSheetBackgroundColor,
this.bottomSheetPhotosTextStyle,
this.bottomSheetCloseIconColor,
});
/// The background color for the [GalleryFooter] widget.
///
/// Defaults to [ColorTheme.barsBg].
final Color? backgroundColor;
/// The color for the "share" icon.
///
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? shareIconColor;
/// The [TextStyle] to use for the [GalleryFooter] title text.
///
/// Defaults to [TextTheme.headlineBold].
final TextStyle? titleTextStyle;
/// The color to use for the "grid" icon.
///
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? gridIconButtonColor;
/// The color to use behind the bottom sheet.
///
/// Defaults to [ColorTheme.overlay].
final Color? bottomSheetBarrierColor;
/// The background color to use for the bottom sheet.
///
/// Defaults to [ColorTheme.barsBg].
final Color? bottomSheetBackgroundColor;
/// The [TextStyle] to use for the "photos" text in the bottom sheet.
///
/// Defaults to [TextTheme.headlineBold].
final TextStyle? bottomSheetPhotosTextStyle;
/// The color to use for the "close" icon.
///
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? bottomSheetCloseIconColor;
/// Copies this [GalleryFooterThemeData] to another.
GalleryFooterThemeData copyWith({
Color? backgroundColor,
Color? shareIconColor,
TextStyle? titleTextStyle,
Color? gridIconButtonColor,
Color? bottomSheetBarrierColor,
Color? bottomSheetBackgroundColor,
TextStyle? bottomSheetPhotosTextStyle,
Color? bottomSheetCloseIconColor,
}) =>
GalleryFooterThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
shareIconColor: shareIconColor ?? this.shareIconColor,
titleTextStyle: titleTextStyle ?? this.titleTextStyle,
gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor,
bottomSheetBarrierColor:
bottomSheetBarrierColor ?? this.bottomSheetBarrierColor,
bottomSheetBackgroundColor:
bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor,
bottomSheetPhotosTextStyle:
bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle,
bottomSheetCloseIconColor:
bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor,
);
/// Linearly interpolate between two [GalleryFooter] themes.
///
/// All the properties must be non-null.
GalleryFooterThemeData lerp(
GalleryFooterThemeData a,
GalleryFooterThemeData b,
double t,
) =>
GalleryFooterThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t),
titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t),
gridIconButtonColor:
Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t),
bottomSheetBarrierColor:
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
bottomSheetBackgroundColor: Color.lerp(
a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, t),
bottomSheetPhotosTextStyle: TextStyle.lerp(
a.bottomSheetPhotosTextStyle, b.bottomSheetPhotosTextStyle, t),
bottomSheetCloseIconColor: Color.lerp(
a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t),
);
/// Merges one [GalleryFooterThemeData] with the another
GalleryFooterThemeData merge(GalleryFooterThemeData? other) {
if (other == null) return this;
return copyWith(
backgroundColor: other.backgroundColor,
bottomSheetBarrierColor: other.bottomSheetBarrierColor,
bottomSheetBackgroundColor: other.bottomSheetBackgroundColor,
bottomSheetCloseIconColor: other.bottomSheetCloseIconColor,
bottomSheetPhotosTextStyle: other.bottomSheetPhotosTextStyle,
gridIconButtonColor: other.gridIconButtonColor,
titleTextStyle: other.titleTextStyle,
shareIconColor: other.shareIconColor,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GalleryFooterThemeData &&
runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor &&
shareIconColor == other.shareIconColor &&
titleTextStyle == other.titleTextStyle &&
gridIconButtonColor == other.gridIconButtonColor &&
bottomSheetBarrierColor == other.bottomSheetBarrierColor &&
bottomSheetBackgroundColor == other.bottomSheetBackgroundColor &&
bottomSheetPhotosTextStyle == other.bottomSheetPhotosTextStyle &&
bottomSheetCloseIconColor == other.bottomSheetCloseIconColor;
@override
int get hashCode =>
backgroundColor.hashCode ^
shareIconColor.hashCode ^
titleTextStyle.hashCode ^
gridIconButtonColor.hashCode ^
bottomSheetBarrierColor.hashCode ^
bottomSheetBackgroundColor.hashCode ^
bottomSheetPhotosTextStyle.hashCode ^
bottomSheetCloseIconColor.hashCode;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ColorProperty('backgroundColor', backgroundColor))
..add(ColorProperty('shareIconColor', shareIconColor))
..add(DiagnosticsProperty('titleTextStyle', titleTextStyle))
..add(ColorProperty('gridIconButtonColor', gridIconButtonColor))
..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor))
..add(ColorProperty(
'bottomSheetBackgroundColor', bottomSheetBackgroundColor))
..add(DiagnosticsProperty(
'bottomSheetPhotosTextStyle', bottomSheetPhotosTextStyle))
..add(ColorProperty(
'bottomSheetCloseIconColor', bottomSheetCloseIconColor));
}
}
@@ -151,9 +151,8 @@ class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
color:
StreamChatTheme.of(context).colorTheme.disabled,
),
),
child: widget.backgroundIcon,
@@ -32,7 +32,7 @@ class SystemMessage extends StatelessWidget {
textAlign: TextAlign.center,
softWrap: true,
style: theme.textTheme.captionBold.copyWith(
color: theme.colorTheme.grey,
color: theme.colorTheme.textLowEmphasis,
),
),
);
@@ -59,7 +59,7 @@ class UploadProgressIndicator extends StatelessWidget {
'${_percentage.toInt()}%',
style: textStyle ??
theme.textTheme.footnote.copyWith(
color: theme.colorTheme.white,
color: theme.colorTheme.barsBg,
),
),
],
@@ -60,7 +60,7 @@ class UrlAttachment extends StatelessWidget {
borderRadius: const BorderRadius.only(
topRight: Radius.circular(16),
),
color: chatThemeData.colorTheme.blueAlice,
color: chatThemeData.colorTheme.linkBg,
),
child: Padding(
padding: const EdgeInsets.only(
@@ -71,7 +71,7 @@ class UrlAttachment extends StatelessWidget {
child: Text(
hostDisplayName,
style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentBlue,
color: chatThemeData.colorTheme.accentPrimary,
),
),
),
@@ -70,7 +70,7 @@ class UserAvatar extends StatelessWidget {
constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme?.constraints,
decoration: BoxDecoration(
color: streamChatTheme.colorTheme.accentBlue,
color: streamChatTheme.colorTheme.accentPrimary,
),
child: hasImage
? CachedNetworkImage(
@@ -95,7 +95,7 @@ class UserAvatar extends StatelessWidget {
child: Container(
constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme?.constraints,
color: selectionColor ?? streamChatTheme.colorTheme.accentBlue,
color: selectionColor ?? streamChatTheme.colorTheme.accentPrimary,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: avatar,
@@ -115,7 +115,7 @@ class UserAvatar extends StatelessWidget {
alignment: onlineIndicatorAlignment,
child: Material(
type: MaterialType.circle,
color: streamChatTheme.colorTheme.white,
color: streamChatTheme.colorTheme.barsBg,
child: Container(
margin: const EdgeInsets.all(2),
constraints: onlineIndicatorConstraints ??
@@ -125,7 +125,7 @@ class UserAvatar extends StatelessWidget {
),
child: Material(
shape: const CircleBorder(),
color: streamChatTheme.colorTheme.accentGreen,
color: streamChatTheme.colorTheme.accentInfo,
),
),
),
@@ -76,7 +76,7 @@ class UserItem extends StatelessWidget {
),
trailing: selected
? StreamSvgIcon.checkSend(
color: chatThemeData.colorTheme.accentBlue,
color: chatThemeData.colorTheme.accentPrimary,
)
: null,
title: Text(
@@ -94,8 +94,8 @@ class UserItem extends StatelessWidget {
? context.translations.userOnlineText
: context.translations.userLastOnlineText +
Jiffy(user.lastActive).fromNow(),
style: chatTheme.textTheme.footnote
.copyWith(color: chatTheme.colorTheme.black.withOpacity(.5)),
style: chatTheme.textTheme.footnote.copyWith(
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(.5)),
);
}
}
@@ -287,7 +287,7 @@ class _UserListViewState extends State<UserListView>
final chatThemeData = StreamChatTheme.of(context);
return Container(
key: ValueKey<String>('HEADER-$header'),
color: chatThemeData.colorTheme.black.withOpacity(0.05),
color: chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.05),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Text(
@@ -295,7 +295,7 @@ class _UserListViewState extends State<UserListView>
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.5,
color: chatThemeData.colorTheme.grey,
color: chatThemeData.colorTheme.textLowEmphasis,
),
),
),
@@ -387,7 +387,7 @@ class _UserListViewState extends State<UserListView>
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.accentError
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -410,6 +410,6 @@ class _UserListViewState extends State<UserListView>
Widget _separatorBuilder(context, i) => Container(
height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
color: StreamChatTheme.of(context).colorTheme.borders,
);
}
@@ -29,7 +29,7 @@ Future<bool?> showConfirmationDialog(
final chatThemeData = StreamChatTheme.of(context);
return showModalBottomSheet(
useRootNavigator: false,
backgroundColor: chatThemeData.colorTheme.white,
backgroundColor: chatThemeData.colorTheme.barsBg,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
@@ -73,7 +73,7 @@ Future<bool?> showConfirmationDialog(
child: Text(
cancelText,
style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.black
color: chatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5)),
),
),
@@ -89,7 +89,7 @@ Future<bool?> showConfirmationDialog(
child: Text(
okText,
style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentRed),
color: chatThemeData.colorTheme.accentError),
),
),
),
@@ -114,7 +114,8 @@ Future<bool?> showInfoDialog(
final chatThemeData = StreamChatTheme.of(context);
return showModalBottomSheet(
useRootNavigator: false,
backgroundColor: theme?.colorTheme.white ?? chatThemeData.colorTheme.white,
backgroundColor:
theme?.colorTheme.barsBg ?? chatThemeData.colorTheme.barsBg,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
@@ -145,8 +146,8 @@ Future<bool?> showInfoDialog(
height: 36,
),
Container(
color: theme?.colorTheme.black.withOpacity(.08) ??
chatThemeData.colorTheme.black.withOpacity(.08),
color: theme?.colorTheme.textHighEmphasis.withOpacity(.08) ??
chatThemeData.colorTheme.textHighEmphasis.withOpacity(.08),
height: 1,
),
Center(
@@ -157,8 +158,8 @@ Future<bool?> showInfoDialog(
child: Text(
okText,
style: TextStyle(
color: theme?.colorTheme.black.withOpacity(0.5) ??
chatThemeData.colorTheme.accentBlue,
color: theme?.colorTheme.textHighEmphasis.withOpacity(0.5) ??
chatThemeData.colorTheme.accentPrimary,
fontWeight: FontWeight.w400,
),
),
@@ -16,21 +16,25 @@ class IVideoService {
/// compress video from [path]
/// compress video from [path] return [Future<MediaInfo>]
///
/// you can choose its quality by [quality],
/// determine whether to delete his source file by [deleteOrigin]
/// optional parameters [startTime] [duration] [includeAudio] [frameRate]
/// you can choose its [quality] and [frameRate]
///
/// ## example
/// ```dart
/// final info = await _flutterVideoCompress.compressVideo(
/// file.path,
/// deleteOrigin: true,
/// );
/// debugPrint(info.toJson());
/// ```
Future<MediaInfo?> compressVideo(String path) async => _lock.synchronized(
Future<MediaInfo?> compressVideo(
String path, {
int frameRate = 30,
VideoQuality quality = VideoQuality.DefaultQuality,
}) async =>
_lock.synchronized(
() => VideoCompress.compressVideo(
path,
frameRate: frameRate,
quality: quality,
),
);
@@ -94,8 +94,8 @@ class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
constraints: const BoxConstraints.expand(),
child: widget.placeholderBuilder?.call(context) ??
Shimmer.fromColors(
baseColor: _streamChatTheme.colorTheme.greyGainsboro,
highlightColor: _streamChatTheme.colorTheme.whiteSmoke,
baseColor: _streamChatTheme.colorTheme.disabled,
highlightColor: _streamChatTheme.colorTheme.inputBg,
child: Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
@@ -2,8 +2,8 @@ export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
export 'src/attachment/attachment.dart';
export 'src/back_button.dart';
export 'src/channel_avatar.dart';
export 'src/channel_header.dart';
export 'src/channel_image.dart';
export 'src/channel_list_header.dart';
export 'src/channel_list_view.dart';
export 'src/channel_name.dart';
@@ -12,8 +12,8 @@ export 'src/connection_status_builder.dart';
export 'src/date_divider.dart';
export 'src/deleted_message.dart';
export 'src/full_screen_media.dart';
export 'src/image_footer.dart';
export 'src/image_header.dart';
export 'src/gallery_footer.dart';
export 'src/gallery_header.dart';
export 'src/info_tile.dart';
export 'src/localization/stream_chat_localizations.dart';
export 'src/mention_tile.dart';
+3 -3
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
version: 2.0.0-nullsafety.8
version: 2.0.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -26,7 +26,7 @@ dependencies:
flutter_svg: ^0.22.0
http_parser: ^4.0.0
image_gallery_saver: ^1.6.9
image_picker: ^0.8.0
image_picker: ^0.8.2
jiffy: ^4.1.0
lottie: ^1.0.1
meta: ^1.3.0
@@ -37,7 +37,7 @@ dependencies:
scrollable_positioned_list: ^0.2.0-nullsafety.0
share_plus: ^2.0.3
shimmer: ^2.0.0
stream_chat_flutter_core: ^2.0.0-nullsafety.8
stream_chat_flutter_core: ^2.0.0
substring_highlight: ^1.0.26
synchronized: ^3.0.0
url_launcher: ^6.0.3
@@ -14,10 +14,12 @@ void main() {
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();
final user = OwnUser(id: 'user-id');
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.user).thenReturn(user);
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -63,7 +65,7 @@ void main() {
));
expect(find.text('test'), findsOneWidget);
expect(find.byType(ChannelImage), findsOneWidget);
expect(find.byType(ChannelAvatar), findsOneWidget);
expect(find.byType(StreamBackButton), findsOneWidget);
expect(find.byType(ChannelInfo), findsOneWidget);
},
@@ -76,10 +78,12 @@ void main() {
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();
final user = OwnUser(id: 'user-id');
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.user).thenReturn(user);
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -141,10 +145,12 @@ void main() {
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();
final user = OwnUser(id: 'user-id');
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.user).thenReturn(user);
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -207,10 +213,12 @@ void main() {
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();
final user = OwnUser(id: 'user-id');
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.user).thenReturn(user);
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -263,7 +271,7 @@ void main() {
expect(find.text('test'), findsNothing);
expect(find.byType(StreamBackButton), findsNothing);
expect(find.byType(ChannelImage), findsNothing);
expect(find.byType(ChannelAvatar), findsNothing);
expect(find.byType(ChannelInfo), findsNothing);
expect(find.text('leading'), findsOneWidget);
expect(find.text('title'), findsOneWidget);
@@ -281,10 +289,12 @@ void main() {
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();
final user = OwnUser(id: 'user-id');
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.user).thenReturn(user);
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -346,10 +356,12 @@ void main() {
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();
final user = OwnUser(id: 'user-id');
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.user).thenReturn(user);
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -403,7 +415,7 @@ void main() {
));
await tester.tap(find.byType(StreamBackButton));
await tester.tap(find.byType(ChannelImage));
await tester.tap(find.byType(ChannelAvatar));
await tester.tap(find.byType(ChannelName));
expect(backPressed, true);
@@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter/src/group_image.dart';
import 'package:stream_chat_flutter/src/group_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'mocks.dart';
@@ -35,7 +35,7 @@ void main() {
child: StreamChannel(
channel: channel,
child: const Scaffold(
body: ChannelImage(),
body: ChannelAvatar(),
),
),
),
@@ -74,7 +74,7 @@ void main() {
userId: 'user-id2',
user: User(
id: 'user-id2',
extraData: {
extraData: const {
'image': 'testimage',
},
),
@@ -85,7 +85,7 @@ void main() {
userId: 'user-id2',
user: User(
id: 'user-id2',
extraData: {
extraData: const {
'image': 'testimage',
},
),
@@ -98,7 +98,7 @@ void main() {
when(() => clientState.usersStream).thenAnswer((i) => Stream.value({
'user-id2': User(
id: 'user-id2',
extraData: {
extraData: const {
'image': 'testimage',
},
),
@@ -113,7 +113,7 @@ void main() {
child: StreamChannel(
channel: channel,
child: const Scaffold(
body: ChannelImage(),
body: ChannelAvatar(),
),
),
),
@@ -132,9 +132,10 @@ void main() {
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();
final currentUser = OwnUser(id: 'user-id');
when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.user).thenReturn(currentUser);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
@@ -143,41 +144,12 @@ void main() {
when(() => channel.extraData).thenReturn({
'name': 'test',
});
when(() => channelState.membersStream).thenAnswer((i) => Stream.value([
Member(
userId: 'user-id',
user: User(
id: 'user-id',
extraData: {
'image': 'testimage1',
},
),
),
Member(
userId: 'user-id2',
user: User(
id: 'user-id2',
extraData: {
'image': 'testimage2',
},
),
),
Member(
userId: 'user-id3',
user: User(
id: 'user-id3',
extraData: {
'image': 'testimage3',
},
),
),
]));
when(() => channelState.members).thenReturn([
final members = [
Member(
userId: 'user-id',
user: User(
id: 'user-id',
extraData: {
extraData: const {
'image': 'testimage1',
},
),
@@ -186,7 +158,7 @@ void main() {
userId: 'user-id2',
user: User(
id: 'user-id2',
extraData: {
extraData: const {
'image': 'testimage2',
},
),
@@ -195,12 +167,15 @@ void main() {
userId: 'user-id3',
user: User(
id: 'user-id3',
extraData: {
extraData: const {
'image': 'testimage3',
},
),
),
]);
];
when(() => channelState.members).thenReturn(members);
when(() => channelState.membersStream)
.thenAnswer((_) => Stream.value(members));
await tester.pumpWidget(MaterialApp(
home: StreamChat(
@@ -208,17 +183,18 @@ void main() {
child: StreamChannel(
channel: channel,
child: const Scaffold(
body: ChannelImage(),
body: ChannelAvatar(),
),
),
),
));
final image = tester.widget<GroupImage>(find.byType(GroupImage));
expect(image.images, [
'testimage2',
'testimage3',
]);
final image = tester.widget<GroupAvatar>(find.byType(GroupAvatar));
final otherMembers = members.where((it) => it.userId != currentUser.id);
expect(
image.members.map((it) => it.user?.id),
otherMembers.map((it) => it.user?.id),
);
},
);
@@ -249,7 +225,7 @@ void main() {
child: StreamChannel(
channel: channel,
child: const Scaffold(
body: ChannelImage(
body: ChannelAvatar(
selected: true,
),
),
@@ -13,11 +13,13 @@ void main() {
final clientState = MockClientState();
final channel = MockChannel();
final channelState = MockChannelState();
final user = OwnUser(id: 'user-id');
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
when(() => channel.cid).thenReturn('cid');
when(() => client.state).thenReturn(clientState);
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
when(() => clientState.user).thenReturn(user);
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -78,7 +80,7 @@ void main() {
expect(find.text('test name'), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(find.text('hello'), findsOneWidget);
expect(find.byType(ChannelImage), findsOneWidget);
expect(find.byType(ChannelAvatar), findsOneWidget);
},
);
}
@@ -70,6 +70,7 @@ void main() {
StreamChat(
streamChatThemeData: theme,
client: client,
connectivityStream: Stream.value(ConnectivityResult.mobile),
child: StreamChannel(
showLoading: false,
channel: channel,
@@ -79,7 +80,6 @@ void main() {
),
),
),
connectivityStream: Stream.value(ConnectivityResult.mobile),
),
),
surfaceSize: const Size.square(200),
@@ -123,6 +123,7 @@ void main() {
StreamChat(
streamChatThemeData: theme,
client: client,
connectivityStream: Stream.value(ConnectivityResult.mobile),
child: StreamChannel(
showLoading: false,
channel: channel,
@@ -132,7 +133,6 @@ void main() {
),
),
),
connectivityStream: Stream.value(ConnectivityResult.mobile),
),
),
surfaceSize: const Size.square(200),
@@ -176,6 +176,7 @@ void main() {
StreamChat(
streamChatThemeData: theme,
client: client,
connectivityStream: Stream.value(ConnectivityResult.mobile),
child: StreamChannel(
showLoading: false,
channel: channel,
@@ -189,7 +190,6 @@ void main() {
),
),
),
connectivityStream: Stream.value(ConnectivityResult.mobile),
),
),
surfaceSize: const Size.square(200),
@@ -54,12 +54,12 @@ void main() {
)
]));
when(() => channelState.typingEvents).thenAnswer((i) => {
User(id: 'other-user', extraData: {'name': 'demo'}):
User(id: 'other-user', extraData: const {'name': 'demo'}):
Event(type: EventType.typingStart),
});
when(() => channelState.typingEventsStream)
.thenAnswer((i) => Stream.value({
User(id: 'other-user', extraData: {'name': 'demo'}):
User(id: 'other-user', extraData: const {'name': 'demo'}):
Event(type: EventType.typingStart),
}));
@@ -0,0 +1,185 @@
import 'package:flutter/material.dart' hide TextTheme;
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:mocktail/mocktail.dart';
class MockStreamChatClient extends Mock implements StreamChatClient {}
void main() {
test('GalleryFooterThemeData copyWith, ==, hashCode basics', () {
expect(const GalleryFooterThemeData(),
const GalleryFooterThemeData().copyWith());
expect(const GalleryFooterThemeData().hashCode,
const GalleryFooterThemeData().copyWith().hashCode);
});
test(
'''Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData''',
() {
expect(
const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl,
_galleryFooterThemeDataControlDark, 1),
_galleryFooterThemeDataControlDark);
});
test(
'''Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData''',
() {
expect(
const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl,
_galleryFooterThemeDataControlDark, 0.5),
_galleryFooterThemeDataControlMidLerp);
});
test(
'''Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData''',
() {
expect(
const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControlDark,
_galleryFooterThemeDataControl, 1),
_galleryFooterThemeDataControl);
});
test('Merging dark and light themes results in a dark theme', () {
expect(
_galleryFooterThemeDataControl
.merge(_galleryFooterThemeDataControlDark),
_galleryFooterThemeDataControlDark);
});
test('Merging dark and light themes results in a dark theme', () {
expect(
_galleryFooterThemeDataControlDark
.merge(_galleryFooterThemeDataControl),
_galleryFooterThemeDataControl);
});
testWidgets(
'Passing no GalleryFooterThemeData returns default light theme values',
(WidgetTester tester) async {
late BuildContext _context;
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => StreamChat(
client: MockStreamChatClient(),
child: child,
),
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: GalleryFooter(
message: Message(),
),
);
},
),
),
);
final imageFooterTheme = GalleryFooterTheme.of(_context);
expect(imageFooterTheme.backgroundColor,
_galleryFooterThemeDataControl.backgroundColor);
expect(imageFooterTheme.shareIconColor,
_galleryFooterThemeDataControl.shareIconColor);
expect(imageFooterTheme.titleTextStyle,
_galleryFooterThemeDataControl.titleTextStyle);
expect(imageFooterTheme.gridIconButtonColor,
_galleryFooterThemeDataControl.gridIconButtonColor);
expect(imageFooterTheme.bottomSheetBarrierColor,
_galleryFooterThemeDataControl.bottomSheetBarrierColor);
expect(imageFooterTheme.bottomSheetBackgroundColor,
_galleryFooterThemeDataControl.bottomSheetBackgroundColor);
expect(imageFooterTheme.bottomSheetCloseIconColor,
_galleryFooterThemeDataControl.bottomSheetCloseIconColor);
expect(imageFooterTheme.bottomSheetPhotosTextStyle,
_galleryFooterThemeDataControl.bottomSheetPhotosTextStyle);
});
testWidgets(
'Passing no GalleryFooterThemeData returns default dark theme values',
(WidgetTester tester) async {
late BuildContext _context;
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => StreamChat(
client: MockStreamChatClient(),
streamChatThemeData: StreamChatThemeData.dark(),
child: child,
),
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: GalleryFooter(
message: Message(),
),
);
},
),
),
);
final imageFooterTheme = GalleryFooterTheme.of(_context);
expect(imageFooterTheme.backgroundColor,
_galleryFooterThemeDataControlDark.backgroundColor);
expect(imageFooterTheme.shareIconColor,
_galleryFooterThemeDataControlDark.shareIconColor);
expect(imageFooterTheme.titleTextStyle,
_galleryFooterThemeDataControlDark.titleTextStyle);
expect(imageFooterTheme.gridIconButtonColor,
_galleryFooterThemeDataControlDark.gridIconButtonColor);
expect(imageFooterTheme.bottomSheetBarrierColor,
_galleryFooterThemeDataControlDark.bottomSheetBarrierColor);
expect(imageFooterTheme.bottomSheetBackgroundColor,
_galleryFooterThemeDataControlDark.bottomSheetBackgroundColor);
expect(imageFooterTheme.bottomSheetCloseIconColor,
_galleryFooterThemeDataControlDark.bottomSheetCloseIconColor);
expect(imageFooterTheme.bottomSheetPhotosTextStyle,
_galleryFooterThemeDataControlDark.bottomSheetPhotosTextStyle);
});
}
// Light theme control
final _galleryFooterThemeDataControl = GalleryFooterThemeData(
backgroundColor: ColorTheme.light().barsBg,
shareIconColor: ColorTheme.light().textHighEmphasis,
titleTextStyle: TextTheme.light().headlineBold,
gridIconButtonColor: ColorTheme.light().textHighEmphasis,
bottomSheetBackgroundColor: ColorTheme.light().barsBg,
bottomSheetBarrierColor: ColorTheme.light().overlay,
bottomSheetCloseIconColor: ColorTheme.light().textHighEmphasis,
bottomSheetPhotosTextStyle: TextTheme.light().headlineBold,
);
// Mid-lerp theme control
const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData(
backgroundColor: Color(0xff87898b),
shareIconColor: Color(0xff7f7f7f),
titleTextStyle: TextStyle(
color: Color(0xff7f7f7f),
fontSize: 16,
fontWeight: FontWeight.bold,
),
gridIconButtonColor: Color(0xff7f7f7f),
bottomSheetBarrierColor: Color(0x4c000000),
bottomSheetBackgroundColor: Color(0xff87898b),
bottomSheetPhotosTextStyle: TextStyle(
color: Color(0xff7f7f7f),
fontSize: 16,
fontWeight: FontWeight.bold,
),
bottomSheetCloseIconColor: Color(0xff7f7f7f),
);
// Dark theme control
final _galleryFooterThemeDataControlDark = GalleryFooterThemeData(
backgroundColor: ColorTheme.dark().barsBg,
shareIconColor: ColorTheme.dark().textHighEmphasis,
titleTextStyle: TextTheme.dark().headlineBold,
gridIconButtonColor: ColorTheme.dark().textHighEmphasis,
bottomSheetBackgroundColor: ColorTheme.dark().barsBg,
bottomSheetBarrierColor: ColorTheme.dark().overlay,
bottomSheetCloseIconColor: ColorTheme.dark().textHighEmphasis,
bottomSheetPhotosTextStyle: TextTheme.dark().headlineBold,
);
@@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:mocktail/mocktail.dart';
class MockStreamChatClient extends Mock implements StreamChatClient {}
void main() {
test('GalleryHeaderThemeData copyWith, ==, hashCode basics', () {
expect(const GalleryHeaderThemeData(),
const GalleryHeaderThemeData().copyWith());
expect(const GalleryHeaderThemeData().hashCode,
const GalleryHeaderThemeData().copyWith().hashCode);
});
test(
'''Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData''',
() {
expect(
const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl,
_galleryHeaderThemeDataDarkControl, 1),
_galleryHeaderThemeDataDarkControl);
});
test(
'''Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData''',
() {
expect(
const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl,
_galleryHeaderThemeDataDarkControl, 0.5),
_galleryHeaderThemeDataHalfLerpControl);
});
test(
'''Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData''',
() {
expect(
const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataDarkControl,
_galleryHeaderThemeDataControl, 1),
_galleryHeaderThemeDataControl);
});
test('Merging dark and light themes results in a dark theme', () {
expect(
_galleryHeaderThemeDataControl
.merge(_galleryHeaderThemeDataDarkControl),
_galleryHeaderThemeDataDarkControl);
});
test('Merging dark and light themes results in a dark theme', () {
expect(
_galleryHeaderThemeDataDarkControl
.merge(_galleryHeaderThemeDataControl),
_galleryHeaderThemeDataControl);
});
testWidgets(
'Passing no GalleryHeaderThemeData returns default light theme values',
(WidgetTester tester) async {
late BuildContext _context;
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => StreamChat(
client: MockStreamChatClient(),
child: child,
),
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: GalleryHeader(
message: Message(),
),
);
},
),
),
);
final imageHeaderTheme = GalleryHeaderTheme.of(_context);
expect(imageHeaderTheme.closeButtonColor,
_galleryHeaderThemeDataControl.closeButtonColor);
expect(imageHeaderTheme.backgroundColor,
_galleryHeaderThemeDataControl.backgroundColor);
expect(imageHeaderTheme.iconMenuPointColor,
_galleryHeaderThemeDataControl.iconMenuPointColor);
expect(imageHeaderTheme.titleTextStyle,
_galleryHeaderThemeDataControl.titleTextStyle);
expect(imageHeaderTheme.subtitleTextStyle,
_galleryHeaderThemeDataControl.subtitleTextStyle);
expect(imageHeaderTheme.bottomSheetBarrierColor,
_galleryHeaderThemeDataControl.bottomSheetBarrierColor);
});
testWidgets(
'Passing no GalleryHeaderThemeData returns default dark theme values',
(WidgetTester tester) async {
late BuildContext _context;
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => StreamChat(
client: MockStreamChatClient(),
streamChatThemeData: StreamChatThemeData.dark(),
child: child,
),
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: GalleryHeader(
message: Message(),
),
);
},
),
),
);
final imageHeaderTheme = GalleryHeaderTheme.of(_context);
expect(imageHeaderTheme.closeButtonColor,
_galleryHeaderThemeDataDarkControl.closeButtonColor);
expect(imageHeaderTheme.backgroundColor,
_galleryHeaderThemeDataDarkControl.backgroundColor);
expect(imageHeaderTheme.iconMenuPointColor,
_galleryHeaderThemeDataDarkControl.iconMenuPointColor);
expect(imageHeaderTheme.titleTextStyle,
_galleryHeaderThemeDataDarkControl.titleTextStyle);
expect(imageHeaderTheme.subtitleTextStyle,
_galleryHeaderThemeDataDarkControl.subtitleTextStyle);
expect(imageHeaderTheme.bottomSheetBarrierColor,
_galleryHeaderThemeDataDarkControl.bottomSheetBarrierColor);
});
}
// Light theme test control.
final _galleryHeaderThemeDataControl = GalleryHeaderThemeData(
closeButtonColor: const Color(0xff000000),
backgroundColor: const Color(0xffffffff),
iconMenuPointColor: const Color(0xff000000),
titleTextStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
subtitleTextStyle: const TextStyle(
fontSize: 12,
color: Colors.black,
).copyWith(
color: const Color(0xff7A7A7A),
),
bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.2),
);
// Light theme test control.
final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData(
closeButtonColor: const Color(0xff7f7f7f),
backgroundColor: const Color(0xff87898b),
iconMenuPointColor: const Color(0xff7f7f7f),
titleTextStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xff7f7f7f),
),
subtitleTextStyle: const TextStyle(
fontSize: 12,
color: Color(0xff7a7a7a),
).copyWith(
color: const Color(0xff7A7A7A),
),
bottomSheetBarrierColor: const Color(0x4c000000),
);
// Dark theme test control.
final _galleryHeaderThemeDataDarkControl = GalleryHeaderThemeData(
closeButtonColor: const Color(0xffffffff),
backgroundColor: const Color(0xff101418),
iconMenuPointColor: const Color(0xffffffff),
titleTextStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
subtitleTextStyle: const TextStyle(
fontSize: 12,
color: Colors.white,
).copyWith(
color: const Color(0xff7A7A7A),
),
bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.4),
);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

@@ -37,7 +37,7 @@ void main() {
child: WillPopScope(
onWillPop: () async => false,
child: Scaffold(
body: ImageFooter(
body: GalleryFooter(
message: Message(),
),
),
@@ -79,7 +79,8 @@ void main() {
'name': 'test',
});
final messageText = '''a message.
const messageText = '''
a message.
with multiple lines
and a list:
- a. okasd
@@ -47,6 +47,7 @@ void main() {
StreamChat(
client: client,
streamChatThemeData: theme,
connectivityStream: Stream.value(ConnectivityResult.mobile),
child: SizedBox(
child: ReactionBubble(
reactions: [
@@ -60,7 +61,6 @@ void main() {
maskColor: theme.ownMessageTheme.reactionsMaskColor!,
),
),
connectivityStream: Stream.value(ConnectivityResult.mobile),
),
surfaceSize: const Size(100, 100),
);
@@ -83,6 +83,7 @@ void main() {
StreamChat(
client: client,
streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
connectivityStream: Stream.value(ConnectivityResult.mobile),
child: Container(
color: Colors.black,
child: ReactionBubble(
@@ -97,7 +98,6 @@ void main() {
maskColor: theme.ownMessageTheme.reactionsMaskColor!,
),
),
connectivityStream: Stream.value(ConnectivityResult.mobile),
),
surfaceSize: const Size(100, 100),
);
@@ -89,6 +89,7 @@ void main() {
)(
StreamChat(
client: client,
connectivityStream: Stream.value(ConnectivityResult.mobile),
child: StreamChannel(
showLoading: false,
channel: channel,
@@ -100,7 +101,6 @@ void main() {
),
),
),
connectivityStream: Stream.value(ConnectivityResult.mobile),
),
),
surfaceSize: const Size.square(200),
@@ -141,6 +141,7 @@ void main() {
)(
StreamChat(
client: client,
connectivityStream: Stream.value(ConnectivityResult.mobile),
child: StreamChannel(
showLoading: false,
channel: channel,
@@ -152,7 +153,6 @@ void main() {
),
),
),
connectivityStream: Stream.value(ConnectivityResult.mobile),
),
),
surfaceSize: const Size.square(200),
@@ -54,12 +54,12 @@ void main() {
]));
when(() => channelState.typingEvents).thenAnswer((i) => {
User(id: 'other-user', extraData: {'name': 'demo'}):
User(id: 'other-user', extraData: const {'name': 'demo'}):
Event(type: EventType.typingStart),
});
when(() => channelState.typingEventsStream)
.thenAnswer((i) => Stream.value({
User(id: 'other-user', extraData: {'name': 'demo'}):
User(id: 'other-user', extraData: const {'name': 'demo'}):
Event(type: EventType.typingStart),
}));
@@ -1,3 +1,27 @@
## 2.0.0
🛑️ Breaking Changes from `1.5.3`
- migrate this package to null safety
- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties
- `options.state` -> bool state
- `options.watch` -> bool watch
- `options.presence` -> bool presence
- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties
- `options.presence` -> bool presence
✅ Added
- Monitor connection using `connectivity_plus` package
🐞 Fixed
- Minor fixes
- Performance improvements
## 2.0.0-nullsafety.9
- Update llc dependency
## 2.0.0-nullsafety.8
🛑️ Breaking Changes from `2.0.0-nullsafety.7`
+1 -1
View File
@@ -28,7 +28,7 @@ This repo includes a fully functional example app with setup instructions.
The example is available under the [example](https://github.com/GetStream/stream-chat-flutter-core/tree/master/example) folder.
## Add dependency
Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter)
Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter_core.svg)](https://pub.dartlang.org/packages/stream_chat_flutter_core)
```yaml
dependencies:
stream_chat_flutter_core: ^latest_version
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Future<void> main() async {
/// Create a new instance of [StreamChatClient] passing the apikey obtained from your
/// project dashboard.
/// Create a new instance of [StreamChatClient] passing the apikey obtained
/// from your project dashboard.
final client = StreamChatClient('b67pax5b2wdq');
/// Set the current user. In a production scenario, this should be done using
@@ -13,12 +13,13 @@ Future<void> main() async {
await client.connectUser(
User(
id: 'cool-shadow-7',
extraData: {
extraData: const {
'image':
'https://getstream.io/random_png/?id=cool-shadow-7&amp;name=Cool+shadow',
},
),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo',
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9'
'.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo',
);
runApp(
@@ -29,36 +30,36 @@ Future<void> main() async {
}
/// Example application using Stream Chat core widgets.
/// Stream Chat Core is a set of Flutter wrappers which provide basic functionality
/// for building Flutter applications using Stream.
/// Stream Chat Core is a set of Flutter wrappers which provide basic
/// functionality for building Flutter applications using Stream.
///
/// If you'd prefer using pre-made UI widgets for your app, please see our other
/// package, `stream_chat_flutter`.
class StreamExample extends StatelessWidget {
/// Minimal example using Stream's core Flutter package.
/// If you'd prefer using pre-made UI widgets for your app, please see our other
/// package, `stream_chat_flutter`.
///
/// If you'd prefer using pre-made UI widgets for your app, please see our
/// other package, `stream_chat_flutter`.
const StreamExample({
Key? key,
required this.client,
}) : super(key: key);
/// Instance of Stream Client.
/// Stream's [StreamChatClient] can be used to connect to our servers and set the default
/// user for the application. Performing these actions trigger a websocket connection
/// allowing for real-time updates.
/// Stream's [StreamChatClient] can be used to connect to our servers and
/// set the default user for the application. Performing these actions
/// trigger a websocket connection allowing for real-time updates.
final StreamChatClient client;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Stream Chat Core Example',
home: HomeScreen(),
builder: (context, child) => StreamChatCore(
client: client,
child: child!,
),
);
}
Widget build(BuildContext context) => MaterialApp(
title: 'Stream Chat Core Example',
home: HomeScreen(),
builder: (context, child) => StreamChatCore(
client: client,
child: child!,
),
);
}
/// Basic layout displaying a list of [Channel]s the user is a part of.
@@ -67,104 +68,108 @@ class StreamExample extends StatelessWidget {
/// [ChannelListCore] is a `builder` with callbacks for constructing UIs based
/// on different scenarios.
class HomeScreen extends StatelessWidget {
/// Builds a basic layout displaying a list of [Channel]s the user is a
/// part of.
HomeScreen({Key? key}) : super(key: key);
/// Controller used for loading more data and controlling pagination in
/// [ChannelListCore].
final channelListController = ChannelListController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Channels'),
),
body: ChannelsBloc(
child: ChannelListCore(
channelListController: channelListController,
filter: Filter.and([
Filter.equal('type', 'messaging'),
Filter.in_('members', [
StreamChatCore.of(context).user!.id,
])
]),
emptyBuilder: (BuildContext context) {
return Center(
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Channels'),
),
body: ChannelsBloc(
child: ChannelListCore(
channelListController: channelListController,
filter: Filter.and([
Filter.equal('type', 'messaging'),
Filter.in_('members', [
StreamChatCore.of(context).user!.id,
])
]),
emptyBuilder: (BuildContext context) => const Center(
child: Text('Looks like you are not in any channels'),
);
},
loadingBuilder: (BuildContext context) {
return Center(
),
loadingBuilder: (BuildContext context) => const Center(
child: SizedBox(
height: 100.0,
width: 100.0,
height: 100,
width: 100,
child: CircularProgressIndicator(),
),
);
},
errorBuilder: (
BuildContext context,
dynamic error,
) {
return Center(
),
errorBuilder: (
BuildContext context,
dynamic error,
) =>
Center(
child: Text(
'Oh no, something went wrong. Please check your config. ${error}'),
);
},
listBuilder: (
BuildContext context,
List<Channel> channels,
) =>
LazyLoadScrollView(
onEndOfPage: () async {
channelListController.paginateData!();
},
child: ListView.builder(
itemCount: channels.length,
itemBuilder: (BuildContext context, int index) {
final _item = channels[index];
return ListTile(
title: Text(_item.name!),
subtitle: StreamBuilder<Message?>(
stream: _item.state!.lastMessageStream,
initialData: _item.state!.lastMessage,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.text!);
}
return SizedBox();
},
),
onTap: () {
/// Display a list of messages when the user taps on an item.
/// We can use [StreamChannel] to wrap our [MessageScreen] screen
/// with the selected channel.
///
/// This allows us to use a built-in inherited widget for accessing
/// our `channel` later on.
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: _item,
child: MessageScreen(),
),
),
);
},
);
'Oh no, something went wrong. '
'Please check your config. $error',
),
),
listBuilder: (
BuildContext context,
List<Channel> channels,
) =>
LazyLoadScrollView(
onEndOfPage: () async {
channelListController.paginateData!();
},
child: ListView.builder(
itemCount: channels.length,
itemBuilder: (BuildContext context, int index) {
final _item = channels[index];
return ListTile(
title: Text(_item.name!),
subtitle: StreamBuilder<Message?>(
stream: _item.state!.lastMessageStream,
initialData: _item.state!.lastMessage,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.text!);
}
return const SizedBox();
},
),
onTap: () {
/// Display a list of messages when the user taps on
/// an item. We can use [StreamChannel] to wrap our
/// [MessageScreen] screen with the selected channel.
///
/// This allows us to use a built-in inherited widget
/// for accessing our `channel` later on.
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: _item,
child: const MessageScreen(),
),
),
);
},
);
},
),
),
),
),
),
);
}
);
}
/// A list of messages sent in the current channel.
/// When a user taps on a channel in [HomeScreen], a navigator push [MessageScreen]
/// to display the list of messages in the selected channel.
/// When a user taps on a channel in [HomeScreen], a navigator push
/// [MessageScreen] to display the list of messages in the selected channel.
///
/// This is implemented using [MessageListCore], a convenience builder with
/// callbacks for building UIs based on different api results.
class MessageScreen extends StatefulWidget {
/// Build a MessageScreen
const MessageScreen({Key? key}) : super(key: key);
@override
_MessageScreenState createState() => _MessageScreenState();
}
@@ -198,19 +203,19 @@ class _MessageScreenState extends State<MessageScreen> {
@override
Widget build(BuildContext context) {
/// To access the current channel, we can use the `.of()` method on [StreamChannel]
/// to fetch the closest instance.
/// To access the current channel, we can use the `.of()` method on
/// [StreamChannel] to fetch the closest instance.
final channel = StreamChannel.of(context).channel;
return Scaffold(
appBar: AppBar(
title: StreamBuilder<List<User>>(
initialData: channel.state?.typingEvents,
stream: channel.state?.typingEventsStream,
title: StreamBuilder<Iterable<User>>(
initialData: channel.state?.typingEvents.keys,
stream: channel.state?.typingEventsStream.map((it) => it.keys),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
return Text('${snapshot.data!.first.name} is typing...');
}
return SizedBox();
return const SizedBox();
},
),
),
@@ -224,57 +229,52 @@ class _MessageScreenState extends State<MessageScreen> {
},
child: MessageListCore(
messageListController: messageListController,
emptyBuilder: (BuildContext context) {
return Center(
child: Text('Nothing here yet'),
);
},
loadingBuilder: (BuildContext context) {
return Center(
child: SizedBox(
height: 100.0,
width: 100.0,
child: CircularProgressIndicator(),
),
);
},
emptyBuilder: (BuildContext context) => const Center(
child: Text('Nothing here yet'),
),
loadingBuilder: (BuildContext context) => const Center(
child: SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(),
),
),
messageListBuilder: (
BuildContext context,
List<Message> messages,
) {
return ListView.builder(
controller: _scrollController,
itemCount: messages.length,
reverse: true,
itemBuilder: (BuildContext context, int index) {
final item = messages[index];
final client = StreamChatCore.of(context).client;
if (item.user!.id == client.uid) {
return Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.text!),
),
);
} else {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.text!),
),
);
}
},
);
},
errorWidgetBuilder: (BuildContext context, error) {
) =>
ListView.builder(
controller: _scrollController,
itemCount: messages.length,
reverse: true,
itemBuilder: (BuildContext context, int index) {
final item = messages[index];
final client = StreamChatCore.of(context).client;
if (item.user!.id == client.uid) {
return Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(item.text!),
),
);
} else {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(item.text!),
),
);
}
},
),
errorBuilder: (BuildContext context, error) {
print(error.toString());
return Center(
return const Center(
child: SizedBox(
height: 100.0,
width: 100.0,
height: 100,
width: 100,
child:
Text('Oh no, an error occured. Please see logs.'),
),
@@ -344,7 +344,7 @@ extension on Channel {
String? get name {
final _channelName = extraData['name'];
if (_channelName != null) {
return _channelName;
return _channelName as String;
} else {
return cid;
}
@@ -21,14 +21,14 @@ environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.3
flutter:
sdk: flutter
stream_chat_flutter_core:
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.0
dev_dependencies:
flutter_test:
@@ -19,16 +19,15 @@ import 'package:stream_chat_flutter_core/src/typedef.dart';
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: ChannelListCore(
/// filter: {
/// 'members': {
/// '\$in': [StreamChat.of(context).user.id],
/// }
/// },
/// filter: Filter.in_(
/// 'members',
/// [StreamChat.of(context).user!.id],
/// ),
/// sort: [SortOption('last_message_at')],
/// pagination: PaginationParams(
/// limit: 20,
/// ),
/// errorBuilder: (err) {
/// errorBuilder: (context, err) {
/// return Center(
/// child: Text('An error has occured'),
/// );
@@ -38,7 +37,7 @@ import 'package:stream_chat_flutter_core/src/typedef.dart';
/// child: Text('Nothing here...'),
/// );
/// },
/// emptyBuilder: (context) {
/// loadingBuilder: (context) {
/// return Center(
/// child: CircularProgressIndicator(),
/// );
@@ -146,6 +146,8 @@ class ChannelsBlocState extends State<ChannelsBloc>
_paginationEnded = true;
}
} catch (e, stk) {
// reset loading controller
_queryChannelsLoadingController.sink.add(false);
if (_channelsController.hasValue) {
_queryChannelsLoadingController.addError(e, stk);
} else {
@@ -85,6 +85,7 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final extentBefore = notification.metrics.extentBefore;
final extentAfter = notification.metrics.extentAfter;
final scrollingDown = _scrollPosition < pixels;
_scrollPosition = pixels;
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
@@ -97,8 +98,6 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
return true;
}
}
_scrollPosition = pixels;
}
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
@@ -40,7 +40,7 @@ import 'package:stream_chat_flutter_core/src/typedef.dart';
/// messageListBuilder: (context, list) {
/// return MessagesPage(list);
/// },
/// errorWidgetBuilder: (context, err) {
/// errorBuilder: (context, err) {
/// return Center(
/// child: Text('Error'),
/// );
@@ -67,7 +67,7 @@ class MessageListCore extends StatefulWidget {
required this.loadingBuilder,
required this.emptyBuilder,
required this.messageListBuilder,
required this.errorWidgetBuilder,
required this.errorBuilder,
this.parentMessage,
this.messageListController,
this.messageFilter,
@@ -91,7 +91,7 @@ class MessageListCore extends StatefulWidget {
///
/// This parameter can be used to display an error message to users in the
/// event of a connection failure.
final ErrorBuilder errorWidgetBuilder;
final ErrorBuilder errorBuilder;
/// If the current message belongs to a `thread`, this property represents the
/// first message or the parent of the conversation.
@@ -144,7 +144,7 @@ class MessageListCoreState extends State<MessageListCore> {
growable: false,
),
),
errorBuilder: widget.errorWidgetBuilder,
errorBuilder: widget.errorBuilder,
loadingBuilder: widget.loadingBuilder,
builder: (context, data) {
final messageList = data?.reversed.toList(growable: false) ?? [];

Some files were not shown because too many files have changed in this diff Show More