From 9e388f59aa22c1c70efae1d18d63256b22f6c9b0 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 24 May 2023 20:47:44 -0700 Subject: [PATCH 01/11] api: Take anchor as parameter in getMessages --- lib/api/route/messages.dart | 27 ++++++++++++++++++++++++++- lib/model/message_list.dart | 1 + 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/api/route/messages.dart b/lib/api/route/messages.dart index a616f84391..f2382512fd 100644 --- a/lib/api/route/messages.dart +++ b/lib/api/route/messages.dart @@ -7,17 +7,42 @@ part 'messages.g.dart'; /// https://zulip.com/api/get-messages Future getMessages(ApiConnection connection, { + required Anchor anchor, required int numBefore, required int numAfter, }) { return connection.get('getMessages', GetMessagesResult.fromJson, 'messages', { // 'narrow': [], // TODO parametrize - 'anchor': 999999999, // TODO parametrize; use RawParameter for strings + 'anchor': switch (anchor) { + NumericAnchor(:var messageId) => messageId, + AnchorCode.newest => RawParameter('newest'), + AnchorCode.oldest => RawParameter('oldest'), + AnchorCode.firstUnread => RawParameter('first_unread'), + }, 'num_before': numBefore, 'num_after': numAfter, }); } +/// An anchor value for [getMessages]. +/// +/// https://zulip.com/api/get-messages#parameter-anchor +sealed class Anchor { + /// This const constructor allows subclasses to have const constructors. + const Anchor(); +} + +/// An anchor value for [getMessages] other than a specific message ID. +/// +/// https://zulip.com/api/get-messages#parameter-anchor +enum AnchorCode implements Anchor { newest, oldest, firstUnread } + +/// A specific message ID, used as an anchor in [getMessages]. +class NumericAnchor extends Anchor { + const NumericAnchor(this.messageId); + final int messageId; +} + @JsonSerializable(fieldRename: FieldRename.snake) class GetMessagesResult { final int anchor; diff --git a/lib/model/message_list.dart b/lib/model/message_list.dart index 6fb84a9837..d24f23125f 100644 --- a/lib/model/message_list.dart +++ b/lib/model/message_list.dart @@ -58,6 +58,7 @@ class MessageListView extends ChangeNotifier { assert(contents.isEmpty); // TODO schedule all this in another isolate final result = await getMessages(store.connection, + anchor: AnchorCode.newest, // TODO(#80): switch to firstUnread numBefore: 100, numAfter: 10, ); From 69b8e74309b55e194fd6d5d40a5e2f55b9af3fc6 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 24 May 2023 20:13:32 -0700 Subject: [PATCH 02/11] narrow [nfc]: Make Narrow a sealed class --- lib/model/narrow.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/model/narrow.dart b/lib/model/narrow.dart index 1add4197b0..765d2a00ed 100644 --- a/lib/model/narrow.dart +++ b/lib/model/narrow.dart @@ -2,7 +2,7 @@ import '../api/model/model.dart'; /// A Zulip narrow. -abstract class Narrow { +sealed class Narrow { /// This const constructor allows subclasses to have const constructors. const Narrow(); @@ -25,8 +25,6 @@ class AllMessagesNarrow extends Narrow { @override bool operator ==(Object other) { - // Conceptually this is a sealed class, so equality is simplified. - // TODO(dart-3): Make this actually a sealed class. if (other is! AllMessagesNarrow) return false; // Conceptually there's only one value of this type. return true; From 107d5a7283a706433c0883e44d83870f7941c600 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 24 May 2023 20:32:43 -0700 Subject: [PATCH 03/11] narrow: Add StreamNarrow and TopicNarrow --- lib/model/narrow.dart | 47 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/lib/model/narrow.dart b/lib/model/narrow.dart index 765d2a00ed..2d1e48591f 100644 --- a/lib/model/narrow.dart +++ b/lib/model/narrow.dart @@ -6,6 +6,8 @@ sealed class Narrow { /// This const constructor allows subclasses to have const constructors. const Narrow(); + // TODO implement muting; will need containsMessage to take more params + // This means stream muting, topic un/muting, and user muting. bool containsMessage(Message message); } @@ -19,7 +21,6 @@ class AllMessagesNarrow extends Narrow { @override bool containsMessage(Message message) { - // TODO implement muting; will need containsMessage to take more params return true; } @@ -34,4 +35,46 @@ class AllMessagesNarrow extends Narrow { int get hashCode => 'AllMessagesNarrow'.hashCode; } -// TODO other narrow types +class StreamNarrow extends Narrow { + const StreamNarrow(this.streamId); + + final int streamId; + + @override + bool containsMessage(Message message) { + return message is StreamMessage && message.streamId == streamId; + } + + @override + bool operator ==(Object other) { + if (other is! StreamNarrow) return false; + return other.streamId == streamId; + } + + @override + int get hashCode => Object.hash('StreamNarrow', streamId); +} + +class TopicNarrow extends Narrow { + const TopicNarrow(this.streamId, this.topic); + + final int streamId; + final String topic; + + @override + bool containsMessage(Message message) { + return (message is StreamMessage + && message.streamId == streamId && message.subject == topic); + } + + @override + bool operator ==(Object other) { + if (other is! TopicNarrow) return false; + return other.streamId == streamId && other.topic == topic; + } + + @override + int get hashCode => Object.hash('TopicNarrow', streamId, topic); +} + +// TODO other narrow types: PMs/DMs; starred, mentioned; searches; arbitrary From 909c9d91bb99a721f4590b76ee0277da2f22875a Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 30 May 2023 18:16:43 -0700 Subject: [PATCH 04/11] api: Add ApiNarrow type, with JSON serialization --- lib/api/model/narrow.dart | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 lib/api/model/narrow.dart diff --git a/lib/api/model/narrow.dart b/lib/api/model/narrow.dart new file mode 100644 index 0000000000..b944936549 --- /dev/null +++ b/lib/api/model/narrow.dart @@ -0,0 +1,60 @@ +typedef ApiNarrow = List; + +/// An element in the list representing a narrow in the Zulip API. +/// +/// Docs: +/// +/// The existing list of subclasses is incomplete; +/// please add more as needed. +sealed class ApiNarrowElement { + String get operator; + Object get operand; + final bool negated; + + ApiNarrowElement({this.negated = false}); + + Map toJson() => { + 'operator': operator, + 'operand': operand, + if (negated) 'negated': negated, + }; +} + +class ApiNarrowStream extends ApiNarrowElement { + @override String get operator => 'stream'; + + @override final int operand; + + ApiNarrowStream(this.operand, {super.negated}); + + factory ApiNarrowStream.fromJson(Map json) => ApiNarrowStream( + json['operand'] as int, + negated: json['negated'] as bool? ?? false, + ); +} + +class ApiNarrowTopic extends ApiNarrowElement { + @override String get operator => 'topic'; + + @override final String operand; + + ApiNarrowTopic(this.operand, {super.negated}); + + factory ApiNarrowTopic.fromJson(Map json) => ApiNarrowTopic( + json['operand'] as String, + negated: json['negated'] as bool? ?? false, + ); +} + +class ApiNarrowPmWith extends ApiNarrowElement { + @override String get operator => 'pm-with'; // TODO(server-7): use 'dm' where possible + + @override final List operand; + + ApiNarrowPmWith(this.operand, {super.negated}); + + factory ApiNarrowPmWith.fromJson(Map json) => ApiNarrowPmWith( + (json['operand'] as List).map((e) => e as int).toList(), + negated: json['negated'] as bool? ?? false, + ); +} From 2653490ff31fa1054a575a0b898dc4ade4ff0f33 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 30 May 2023 18:27:17 -0700 Subject: [PATCH 05/11] narrow: Add method Narrow.apiEncode --- lib/model/narrow.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/model/narrow.dart b/lib/model/narrow.dart index 2d1e48591f..508e843b02 100644 --- a/lib/model/narrow.dart +++ b/lib/model/narrow.dart @@ -1,5 +1,6 @@ import '../api/model/model.dart'; +import '../api/model/narrow.dart'; /// A Zulip narrow. sealed class Narrow { @@ -9,6 +10,9 @@ sealed class Narrow { // TODO implement muting; will need containsMessage to take more params // This means stream muting, topic un/muting, and user muting. bool containsMessage(Message message); + + /// This narrow, expressed as an [ApiNarrow]. + ApiNarrow apiEncode(); } /// The narrow called "All messages" in the UI. @@ -24,6 +28,9 @@ class AllMessagesNarrow extends Narrow { return true; } + @override + ApiNarrow apiEncode() => []; + @override bool operator ==(Object other) { if (other is! AllMessagesNarrow) return false; @@ -45,6 +52,9 @@ class StreamNarrow extends Narrow { return message is StreamMessage && message.streamId == streamId; } + @override + ApiNarrow apiEncode() => [ApiNarrowStream(streamId)]; + @override bool operator ==(Object other) { if (other is! StreamNarrow) return false; @@ -67,6 +77,9 @@ class TopicNarrow extends Narrow { && message.streamId == streamId && message.subject == topic); } + @override + ApiNarrow apiEncode() => [ApiNarrowStream(streamId), ApiNarrowTopic(topic)]; + @override bool operator ==(Object other) { if (other is! TopicNarrow) return false; From e5c839bcc896887458332160a9a0c97beca94f68 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 24 May 2023 20:07:19 -0700 Subject: [PATCH 06/11] msglist [nfc]: Parameterize MessageListPage on the narrow --- lib/widgets/app.dart | 16 ++++++++++------ lib/widgets/message_list.dart | 8 ++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 13bb241333..748c43ed96 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../model/narrow.dart'; import 'about_zulip.dart'; import 'compose_box.dart'; import 'login.dart'; @@ -143,20 +144,23 @@ class HomePage extends StatelessWidget { const SizedBox(height: 16), ElevatedButton( onPressed: () => Navigator.push(context, - MessageListPage.buildRoute(context)), + MessageListPage.buildRoute(context: context, + narrow: const AllMessagesNarrow())), child: const Text("All messages")), ]))); } } class MessageListPage extends StatelessWidget { - const MessageListPage({super.key}); + const MessageListPage({super.key, required this.narrow}); - static Route buildRoute(BuildContext context) { + static Route buildRoute({required BuildContext context, required Narrow narrow}) { return MaterialAccountPageRoute(context: context, - builder: (context) => const MessageListPage()); + builder: (context) => MessageListPage(narrow: narrow)); } + final Narrow narrow; + @override Widget build(BuildContext context) { return Scaffold( @@ -173,8 +177,8 @@ class MessageListPage extends StatelessWidget { // The compose box pads the bottom inset. removeBottom: true, - child: const Expanded( - child: MessageList())), + child: Expanded( + child: MessageList(narrow: narrow))), const StreamComposeBox(), ])))); } diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index d7e3e3d06f..2f1fb81f82 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -13,15 +13,15 @@ import 'sticky_header.dart'; import 'store.dart'; class MessageList extends StatefulWidget { - const MessageList({super.key}); + const MessageList({super.key, required this.narrow}); + + final Narrow narrow; @override State createState() => _MessageListState(); } class _MessageListState extends State { - Narrow get narrow => const AllMessagesNarrow(); // TODO specify in widget - MessageListView? model; @override @@ -44,7 +44,7 @@ class _MessageListState extends State { } void _initModel(PerAccountStore store) { - model = MessageListView.init(store: store, narrow: narrow); + model = MessageListView.init(store: store, narrow: widget.narrow); model!.addListener(_modelChanged); model!.fetch(); } From a4323c884bc1fe4947c1a5811ddc8a1e43985f41 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 24 May 2023 20:56:13 -0700 Subject: [PATCH 07/11] api: Take narrow as parameter in getMessages --- lib/api/route/messages.dart | 4 +++- lib/model/message_list.dart | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/api/route/messages.dart b/lib/api/route/messages.dart index f2382512fd..41da7b12e5 100644 --- a/lib/api/route/messages.dart +++ b/lib/api/route/messages.dart @@ -2,17 +2,19 @@ import 'package:json_annotation/json_annotation.dart'; import '../core.dart'; import '../model/model.dart'; +import '../model/narrow.dart'; part 'messages.g.dart'; /// https://zulip.com/api/get-messages Future getMessages(ApiConnection connection, { + required ApiNarrow narrow, required Anchor anchor, required int numBefore, required int numAfter, }) { return connection.get('getMessages', GetMessagesResult.fromJson, 'messages', { - // 'narrow': [], // TODO parametrize + 'narrow': narrow, 'anchor': switch (anchor) { NumericAnchor(:var messageId) => messageId, AnchorCode.newest => RawParameter('newest'), diff --git a/lib/model/message_list.dart b/lib/model/message_list.dart index d24f23125f..cb7fbeb3e9 100644 --- a/lib/model/message_list.dart +++ b/lib/model/message_list.dart @@ -58,6 +58,7 @@ class MessageListView extends ChangeNotifier { assert(contents.isEmpty); // TODO schedule all this in another isolate final result = await getMessages(store.connection, + narrow: narrow.apiEncode(), anchor: AnchorCode.newest, // TODO(#80): switch to firstUnread numBefore: 100, numAfter: 10, From e4c85e8321b53557739c8714aa7b22f1a6d60356 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 24 May 2023 20:59:43 -0700 Subject: [PATCH 08/11] api: Make getMessages binding complete --- lib/api/route/messages.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/api/route/messages.dart b/lib/api/route/messages.dart index 41da7b12e5..833d53a14a 100644 --- a/lib/api/route/messages.dart +++ b/lib/api/route/messages.dart @@ -10,8 +10,12 @@ part 'messages.g.dart'; Future getMessages(ApiConnection connection, { required ApiNarrow narrow, required Anchor anchor, + bool? includeAnchor, required int numBefore, required int numAfter, + bool? clientGravatar, + bool? applyMarkdown, + // bool? useFirstUnreadAnchor // omitted because deprecated }) { return connection.get('getMessages', GetMessagesResult.fromJson, 'messages', { 'narrow': narrow, @@ -21,8 +25,11 @@ Future getMessages(ApiConnection connection, { AnchorCode.oldest => RawParameter('oldest'), AnchorCode.firstUnread => RawParameter('first_unread'), }, + if (includeAnchor != null) 'include_anchor': includeAnchor, 'num_before': numBefore, 'num_after': numAfter, + if (clientGravatar != null) 'client_gravatar': clientGravatar, + if (applyMarkdown != null) 'apply_markdown': applyMarkdown, }); } From cc3dfb93759c302105bdd0103d3e00b406aa7237 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 May 2023 14:34:56 -0700 Subject: [PATCH 09/11] api: Take stream or PM conversation as parameter in sendMessage --- lib/api/route/messages.dart | 48 +++++++++++++++++++++------- lib/model/store.dart | 8 ++++- test/api/route/messages_test.dart | 52 ++++++++++++++++++++++++------- 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/lib/api/route/messages.dart b/lib/api/route/messages.dart index 833d53a14a..8c67ad5073 100644 --- a/lib/api/route/messages.dart +++ b/lib/api/route/messages.dart @@ -92,26 +92,52 @@ const int kMaxMessageLengthCodePoints = 10000; const String kNoTopicTopic = '(no topic)'; /// https://zulip.com/api/send-message -// TODO currently only handles stream messages; fix Future sendMessage( ApiConnection connection, { + required MessageDestination destination, required String content, - required String topic, }) { - // assert() is less verbose but would have no effect in production, I think: - // https://dart.dev/guides/language/language-tour#assert - if (connection.realmUrl.origin != 'https://chat.zulip.org') { - throw Exception('This binding can currently only be used on https://chat.zulip.org.'); - } - return connection.post('sendMessage', SendMessageResult.fromJson, 'messages', { - 'type': RawParameter('stream'), // TODO parametrize - 'to': 7, // TODO parametrize; this is `#test here` - 'topic': RawParameter(topic), + if (destination is StreamDestination) ...{ + 'type': RawParameter('stream'), + 'to': destination.streamId, + 'topic': RawParameter(destination.topic), + } else if (destination is PmDestination) ...{ + 'type': RawParameter('private'), // TODO(server-7) + 'to': destination.userIds, + } else ...( + throw Exception('impossible destination') // TODO(dart-3) show this statically + ), 'content': RawParameter(content), }); } +/// Which conversation to send a message to, in [sendMessage]. +/// +/// This is either a [StreamDestination] or a [PmDestination]. +sealed class MessageDestination {} + +/// A conversation in a stream, for specifying to [sendMessage]. +/// +/// The server accepts a stream name as an alternative to a stream ID, +/// but this binding currently doesn't. +class StreamDestination extends MessageDestination { + StreamDestination(this.streamId, this.topic); + + final int streamId; + final String topic; +} + +/// A PM conversation, for specifying to [sendMessage]. +/// +/// The server accepts a list of Zulip API emails as an alternative to +/// a list of user IDs, but this binding currently doesn't. +class PmDestination extends MessageDestination { + PmDestination({required this.userIds}); + + final List userIds; +} + @JsonSerializable(fieldRename: FieldRename.snake) class SendMessageResult { final int id; diff --git a/lib/model/store.dart b/lib/model/store.dart index 9534740b84..74e7a56498 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -251,7 +251,13 @@ class PerAccountStore extends ChangeNotifier { Future sendStreamMessage({required String topic, 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 sendMessage(connection, topic: topic, content: content); + + if (connection.realmUrl.origin != 'https://chat.zulip.org') { + throw Exception('This method can currently only be used on https://chat.zulip.org.'); + } + final destination = StreamDestination(7, topic); // TODO parametrize; this is `#test here` + + return sendMessage(connection, destination: destination, content: content); } } diff --git a/test/api/route/messages_test.dart b/test/api/route/messages_test.dart index d74e693e87..7e571c7275 100644 --- a/test/api/route/messages_test.dart +++ b/test/api/route/messages_test.dart @@ -1,24 +1,52 @@ +import 'dart:convert'; + import 'package:checks/checks.dart'; +import 'package:http/http.dart' as http; import 'package:test/scaffolding.dart'; import 'package:zulip/api/route/messages.dart'; +import '../../stdlib_checks.dart'; import '../fake_api.dart'; import 'route_checks.dart'; void main() { - test('sendMessage accepts fixture realm', () async { - final connection = FakeApiConnection( - realmUrl: Uri.parse('https://chat.zulip.org/')); - connection.prepare(json: SendMessageResult(id: 42).toJson()); - check(sendMessage(connection, content: 'hello', topic: 'world')) - .completes(it()..id.equals(42)); + test('sendMessage to stream', () { + return FakeApiConnection.with_((connection) async { + const streamId = 123; + const topic = 'world'; + const content = 'hello'; + connection.prepare(json: SendMessageResult(id: 42).toJson()); + final result = await sendMessage(connection, + destination: StreamDestination(streamId, topic), content: content); + check(result).id.equals(42); + check(connection.lastRequest).isNotNull().isA() + ..method.equals('POST') + ..url.path.equals('/api/v1/messages') + ..bodyFields.deepEquals({ + 'type': 'stream', + 'to': streamId.toString(), + 'topic': topic, + 'content': content, + }); + }); }); - test('sendMessage rejects unexpected realm', () async { - final connection = FakeApiConnection( - realmUrl: Uri.parse('https://chat.example/')); - connection.prepare(json: SendMessageResult(id: 42).toJson()); - check(() => sendMessage(connection, content: 'hello', topic: 'world')) - .throws(); + test('sendMessage to PM conversation', () { + return FakeApiConnection.with_((connection) async { + const userIds = [23, 34]; + const content = 'hi there'; + connection.prepare(json: SendMessageResult(id: 42).toJson()); + final result = await sendMessage(connection, + destination: PmDestination(userIds: userIds), content: content); + check(result).id.equals(42); + check(connection.lastRequest).isNotNull().isA() + ..method.equals('POST') + ..url.path.equals('/api/v1/messages') + ..bodyFields.deepEquals({ + 'type': 'private', + 'to': jsonEncode(userIds), + 'content': content, + }); + }); }); } From d990d33e7388cfc6e5a110db0cbf0e2359877828 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 May 2023 14:42:14 -0700 Subject: [PATCH 10/11] api: Make sendMessage binding complete --- lib/api/route/messages.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/api/route/messages.dart b/lib/api/route/messages.dart index 8c67ad5073..20461891c1 100644 --- a/lib/api/route/messages.dart +++ b/lib/api/route/messages.dart @@ -96,6 +96,8 @@ Future sendMessage( ApiConnection connection, { required MessageDestination destination, required String content, + String? queueId, + String? localId, }) { return connection.post('sendMessage', SendMessageResult.fromJson, 'messages', { if (destination is StreamDestination) ...{ @@ -109,6 +111,8 @@ Future sendMessage( throw Exception('impossible destination') // TODO(dart-3) show this statically ), 'content': RawParameter(content), + if (queueId != null) 'queue_id': queueId, + if (localId != null) 'local_id': localId, }); } From ddbb34da8042769b1426559b45a0a7a58d401921 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 May 2023 14:49:05 -0700 Subject: [PATCH 11/11] store: Expose sendMessage for general destinations --- lib/model/store.dart | 12 ++++-------- lib/widgets/compose_box.dart | 10 ++++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/model/store.dart b/lib/model/store.dart index 74e7a56498..6ea7ef178b 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -248,19 +248,15 @@ class PerAccountStore extends ChangeNotifier { } } - Future sendStreamMessage({required String topic, required String content}) { + 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 - - if (connection.realmUrl.origin != 'https://chat.zulip.org') { - throw Exception('This method can currently only be used on https://chat.zulip.org.'); - } - final destination = StreamDestination(7, topic); // TODO parametrize; this is `#test here` - - return sendMessage(connection, destination: destination, content: content); + return _apiSendMessage(connection, destination: destination, content: content); } } +const _apiSendMessage = sendMessage; // Bit ugly; for alternatives, see: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20PerAccountStore.20methods/near/1545809 + /// A [GlobalStore] that uses a live server and live, persistent local database. /// /// The underlying data store is an [AppDatabase] corresponding to a SQLite diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 66fe2559ae..f4f5bcb060 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -517,10 +517,12 @@ class _StreamSendButtonState extends State<_StreamSendButton> { } final store = PerAccountStoreWidget.of(context); - store.sendStreamMessage( - topic: widget.topicController.textNormalized(), - content: widget.contentController.textNormalized(), - ); + if (store.connection.realmUrl.origin != 'https://chat.zulip.org') { + throw Exception('This method can currently only be used on https://chat.zulip.org.'); + } + final destination = StreamDestination(7, widget.topicController.textNormalized()); // TODO parametrize; this is `#test here` + final content = widget.contentController.textNormalized(); + store.sendMessage(destination: destination, content: content); widget.contentController.clear(); }