diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 9276ce22..d9877a4b 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -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 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 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 connectUserWithProvider( - User user, TokenProvider tokenProvider) => - _connectUser(user, provider: tokenProvider); + Future 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 connectAnonymousUser() async { + Future 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 connectGuestUser(User user) async { + Future 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 _connectUser( + Future _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 openConnection() async { + Future 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; diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 7ceffbfa..679abb47 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -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 extraData = const {}, bool banned = false, + List 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? extraData, + bool? banned, + List? teams, + List? channelMutes, + List? devices, + List? 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: []) final List devices; diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index c2960af9..1360d14e 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -47,9 +47,10 @@ class User extends Equatable { /// User role @JsonKey( - includeIfNull: false, - toJson: Serializer.readOnly, - defaultValue: []) + includeIfNull: false, + toJson: Serializer.readOnly, + defaultValue: [], + ) final List teams; /// Date of user creation diff --git a/packages/stream_chat/test/src/api/client_test.dart b/packages/stream_chat/test/src/api/client_test.dart index 13783865..72dec24e 100644 --- a/packages/stream_chat/test/src/api/client_test.dart +++ b/packages/stream_chat/test/src/api/client_test.dart @@ -64,9 +64,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 { @@ -93,9 +91,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`', () { @@ -127,9 +123,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 +169,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 +321,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(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 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(); @@ -366,8 +444,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); @@ -391,8 +468,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); @@ -420,8 +496,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); @@ -446,8 +521,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); diff --git a/packages/stream_chat/test/src/core/models/own_user_test.dart b/packages/stream_chat/test/src/core/models/own_user_test.dart index edaf022e..7aef859d 100644 --- a/packages/stream_chat/test/src/core/models/own_user_test.dart +++ b/packages/stream_chat/test/src/core/models/own_user_test.dart @@ -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); + }); }); } diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index f2b1b514..178510c2 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -204,7 +204,8 @@ void main() { final event = Event(type: EventType.any); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.openConnection()) + .thenAnswer((_) async => OwnUser(id: 'test')); when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -238,7 +239,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.openConnection()) + .thenAnswer((_) async => OwnUser(id: 'test')); when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -327,7 +329,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.openConnection()) + .thenAnswer((_) async => OwnUser(id: 'test')); when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -376,7 +379,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.openConnection()) + .thenAnswer((_) async => OwnUser(id: 'test')); when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.connected); @@ -402,7 +406,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.openConnection()) + .thenAnswer((_) async => OwnUser(id: 'test')); when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected);