diff --git a/lib/model/message.dart b/lib/model/message.dart index d1886b810f..398c252a96 100644 --- a/lib/model/message.dart +++ b/lib/model/message.dart @@ -1,10 +1,14 @@ import 'dart:convert'; +import '../api/core.dart'; import '../api/model/events.dart'; import '../api/model/model.dart'; +import '../api/route/messages.dart'; import '../log.dart'; import 'message_list.dart'; +const _apiSendMessage = sendMessage; // Bit ugly; for alternatives, see: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20PerAccountStore.20methods/near/1545809 + /// The portion of [PerAccountStore] for messages and message lists. mixin MessageStore { /// All known messages, indexed by [Message.id]. @@ -15,6 +19,11 @@ mixin MessageStore { void registerMessageList(MessageListView view); void unregisterMessageList(MessageListView view); + Future sendMessage({ + required MessageDestination destination, + required String content, + }); + /// Reconcile a batch of just-fetched messages with the store, /// mutating the list. /// @@ -29,11 +38,13 @@ mixin MessageStore { } class MessageStoreImpl with MessageStore { - MessageStoreImpl() + MessageStoreImpl({required this.connection}) // There are no messages in InitialSnapshot, so we don't have // a use case for initializing MessageStore with nonempty [messages]. : messages = {}; + final ApiConnection connection; + @override final Map messages; @@ -77,6 +88,17 @@ class MessageStoreImpl with MessageStore { // https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/MessageListView.20lifecycle/near/2086893 } + @override + Future sendMessage({required MessageDestination destination, required String content}) { + // TODO implement outbox; see design at + // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/.23M3881.20Sending.20outbox.20messages.20is.20fraught.20with.20issues/near/1405739 + return _apiSendMessage(connection, + destination: destination, + content: content, + readBySender: true, + ); + } + @override void reconcileMessages(List messages) { // What to do when some of the just-fetched messages are already known? diff --git a/lib/model/store.dart b/lib/model/store.dart index 8c5d5bc44a..1bef907a12 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -360,11 +360,19 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel connection ??= globalStore.apiConnectionFromAccount(account); assert(connection.zulipFeatureLevel == account.zulipFeatureLevel); + final queueId = initialSnapshot.queueId; + if (queueId == null) { + // The queueId is optional in the type, but should only be missing in the + // case of unauthenticated access to a web-public realm. We authenticated. + throw Exception("bad initial snapshot: missing queueId"); + } + final realmUrl = account.realmUrl; final channels = ChannelStoreImpl(initialSnapshot: initialSnapshot); return PerAccountStore._( globalStore: globalStore, connection: connection, + queueId: queueId, realmUrl: realmUrl, realmWildcardMentionPolicy: initialSnapshot.realmWildcardMentionPolicy, realmMandatoryTopics: initialSnapshot.realmMandatoryTopics, @@ -393,7 +401,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel typingStartedExpiryPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds), ), channels: channels, - messages: MessageStoreImpl(), + messages: MessageStoreImpl(connection: connection), unreads: Unreads( initial: initialSnapshot.unreadMsgs, selfUserId: account.userId, @@ -408,6 +416,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel PerAccountStore._({ required GlobalStore globalStore, required this.connection, + required this.queueId, required this.realmUrl, required this.realmWildcardMentionPolicy, required this.realmMandatoryTopics, @@ -447,6 +456,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel final GlobalStore _globalStore; final ApiConnection connection; // TODO(#135): update zulipFeatureLevel with events + final String queueId; UpdateMachine? get updateMachine => _updateMachine; UpdateMachine? _updateMachine; set updateMachine(UpdateMachine? value) { @@ -829,16 +839,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel } } + @override Future sendMessage({required MessageDestination destination, required String content}) { assert(!_disposed); - - // TODO implement outbox; see design at - // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/.23M3881.20Sending.20outbox.20messages.20is.20fraught.20with.20issues/near/1405739 - return _apiSendMessage(connection, - destination: destination, - content: content, - readBySender: true, - ); + return _messages.sendMessage(destination: destination, content: content); } static List _sortCustomProfileFields(List initialCustomProfileFields) { @@ -860,7 +864,6 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel String toString() => '${objectRuntimeType(this, 'PerAccountStore')}#${shortHash(this)}'; } -const _apiSendMessage = sendMessage; // Bit ugly; for alternatives, see: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20PerAccountStore.20methods/near/1545809 const _tryResolveUrl = tryResolveUrl; /// Like [Uri.resolve], but on failure return null instead of throwing. @@ -1031,12 +1034,7 @@ class UpdateMachine { UpdateMachine.fromInitialSnapshot({ required this.store, required InitialSnapshot initialSnapshot, - }) : queueId = initialSnapshot.queueId ?? (() { - // The queueId is optional in the type, but should only be missing in the - // case of unauthenticated access to a web-public realm. We authenticated. - throw Exception("bad initial snapshot: missing queueId"); - })(), - lastEventId = initialSnapshot.lastEventId { + }) : lastEventId = initialSnapshot.lastEventId { store.updateMachine = this; } @@ -1110,7 +1108,6 @@ class UpdateMachine { } final PerAccountStore store; - final String queueId; int lastEventId; bool _disposed = false; @@ -1260,7 +1257,7 @@ class UpdateMachine { final GetEventsResult result; try { result = await getEvents(store.connection, - queueId: queueId, lastEventId: lastEventId); + queueId: store.queueId, lastEventId: lastEventId); if (_disposed) return; } catch (e, stackTrace) { if (_disposed) return; diff --git a/test/model/store_test.dart b/test/model/store_test.dart index 6eba7b1299..1dfbb51273 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -296,6 +296,16 @@ void main() { check(connection).isOpen.isFalse(); })); + test('GlobalStore.perAccount throws if missing queueId', () async { + final globalStore = UpdateMachineTestGlobalStore(accounts: [eg.selfAccount]); + globalStore.prepareRegisterQueueResponse = (connection) { + connection.prepare(json: + deepToJson(eg.initialSnapshot()) as Map + ..['queue_id'] = null); + }; + await check(globalStore.perAccount(eg.selfAccount.id)).throws(); + }); + // TODO test insertAccount group('GlobalStore.updateAccount', () { @@ -772,7 +782,7 @@ void main() { ..method.equals('GET') ..url.path.equals('/api/v1/events') ..url.queryParameters.deepEquals({ - 'queue_id': updateMachine.queueId, + 'queue_id': store.queueId, 'last_event_id': lastEventId.toString(), }); } @@ -937,7 +947,7 @@ void main() { void prepareExpiredEventQueue() { connection.prepare(apiException: eg.apiExceptionBadEventQueueId( - queueId: updateMachine.queueId)); + queueId: store.queueId)); } Future prepareHandleEventError() async { diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 6d2fdeef27..b20aaefca9 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -161,7 +161,7 @@ void main() { updateMachine.poll(); updateMachine.debugPrepareLoopError( - eg.apiExceptionBadEventQueueId(queueId: updateMachine.queueId)); + eg.apiExceptionBadEventQueueId(queueId: store.queueId)); updateMachine.debugAdvanceLoop(); await tester.pump(); // Event queue has been replaced; but the [MessageList] hasn't been