Skip to content

Commit b8ab44e

Browse files
committed
compose_box: Replace compose box with a banner when cannot post in a channel
Fixes: #674
1 parent 7a1280d commit b8ab44e

File tree

3 files changed

+160
-33
lines changed

3 files changed

+160
-33
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@
200200
"@errorBannerDeactivatedDmLabel": {
201201
"description": "Label text for error banner when sending a message to one or multiple deactivated users."
202202
},
203+
"errorBannerCannotPostInChannelLabel": "You do not have permission to post in this channel.",
204+
"@errorBannerCannotPostInChannelLabel": {
205+
"description": "Error-banner text replacing the compose box when you do not have permission to send a message to the channel."
206+
},
203207
"composeBoxAttachFilesTooltip": "Attach files",
204208
"@composeBoxAttachFilesTooltip": {
205209
"description": "Tooltip for compose box icon to attach a file to the message."

lib/widgets/compose_box.dart

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,26 +1158,8 @@ class _FixedDestinationComposeBoxState extends State<_FixedDestinationComposeBox
11581158
super.dispose();
11591159
}
11601160

1161-
Widget? _errorBanner(BuildContext context) {
1162-
if (widget.narrow case DmNarrow(:final otherRecipientIds)) {
1163-
final store = PerAccountStoreWidget.of(context);
1164-
final hasDeactivatedUser = otherRecipientIds.any((id) =>
1165-
!(store.users[id]?.isActive ?? true));
1166-
if (hasDeactivatedUser) {
1167-
return _ErrorBanner(label: ZulipLocalizations.of(context)
1168-
.errorBannerDeactivatedDmLabel);
1169-
}
1170-
}
1171-
return null;
1172-
}
1173-
11741161
@override
11751162
Widget build(BuildContext context) {
1176-
final errorBanner = _errorBanner(context);
1177-
if (errorBanner != null) {
1178-
return _ComposeBoxContainer(child: errorBanner);
1179-
}
1180-
11811163
return _ComposeBoxLayout(
11821164
contentController: _contentController,
11831165
contentFocusNode: _contentFocusNode,
@@ -1215,8 +1197,39 @@ class ComposeBox extends StatelessWidget {
12151197
}
12161198
}
12171199

1200+
Widget? _errorBanner(BuildContext context) {
1201+
final store = PerAccountStoreWidget.of(context);
1202+
final selfUser = store.users[store.selfUserId]!;
1203+
final localizations = ZulipLocalizations.of(context);
1204+
switch (narrow) {
1205+
case ChannelNarrow(:final streamId):
1206+
case TopicNarrow(:final streamId):
1207+
final channel = store.streams[streamId];
1208+
if (channel == null || !store.hasPostingPermission(inChannel: channel,
1209+
user: selfUser, byDate: DateTime.now())) {
1210+
return _ErrorBanner(label: localizations.errorBannerCannotPostInChannelLabel);
1211+
}
1212+
case DmNarrow(:final otherRecipientIds):
1213+
final hasDeactivatedUser = otherRecipientIds.any((id) =>
1214+
!(store.users[id]?.isActive ?? true));
1215+
if (hasDeactivatedUser) {
1216+
return _ErrorBanner(label: localizations.errorBannerDeactivatedDmLabel);
1217+
}
1218+
case CombinedFeedNarrow():
1219+
case MentionsNarrow():
1220+
case StarredMessagesNarrow():
1221+
return null;
1222+
}
1223+
return null;
1224+
}
1225+
12181226
@override
12191227
Widget build(BuildContext context) {
1228+
final errorBanner = _errorBanner(context);
1229+
if (errorBanner != null) {
1230+
return _ComposeBoxContainer(child: errorBanner);
1231+
}
1232+
12201233
final narrow = this.narrow;
12211234
switch (narrow) {
12221235
case ChannelNarrow():

test/widgets/compose_box_test.dart

Lines changed: 125 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ void main() {
4141

4242
Future<GlobalKey<ComposeBoxController>> prepareComposeBox(WidgetTester tester, {
4343
required Narrow narrow,
44+
User? selfUser,
45+
int? realmWaitingPeriodThreshold,
4446
List<User> users = const [],
4547
List<ZulipStream> streams = const [],
4648
}) async {
@@ -49,16 +51,19 @@ void main() {
4951
'Add a channel with "streamId" the same as of $narrow.streamId to the store.');
5052
}
5153
addTearDown(testBinding.reset);
52-
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
54+
selfUser ??= eg.selfUser;
55+
final selfAccount = eg.account(user: selfUser);
56+
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot(
57+
realmWaitingPeriodThreshold: realmWaitingPeriodThreshold));
5358

54-
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
59+
store = await testBinding.globalStore.perAccount(selfAccount.id);
5560

56-
await store.addUsers([eg.selfUser, ...users]);
61+
await store.addUsers([selfUser, ...users]);
5762
await store.addStreams(streams);
5863
connection = store.connection as FakeApiConnection;
5964

6065
final controllerKey = GlobalKey<ComposeBoxController>();
61-
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
66+
await tester.pumpWidget(TestZulipApp(accountId: selfAccount.id,
6267
child: Column(
6368
// This positions the compose box at the bottom of the screen,
6469
// simulating the layout of the message list page.
@@ -303,17 +308,20 @@ void main() {
303308

304309
Future<void> prepareComposeBoxWithNavigation(WidgetTester tester) async {
305310
addTearDown(testBinding.reset);
306-
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
311+
final selfUser = eg.selfUser;
312+
final selfAccount = eg.account(user: selfUser);
313+
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot());
307314

308-
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
315+
store = await testBinding.globalStore.perAccount(selfAccount.id);
316+
await store.addUser(selfUser);
309317
await store.addStream(channel);
310318
connection = store.connection as FakeApiConnection;
311319

312320
await tester.pumpWidget(const ZulipApp());
313321
await tester.pump();
314322
final navigator = await ZulipApp.navigator;
315323
unawaited(navigator.push(MaterialAccountWidgetRoute(
316-
accountId: eg.selfAccount.id, page: ComposeBox(narrow: narrow))));
324+
accountId: selfAccount.id, page: ComposeBox(narrow: narrow))));
317325
await tester.pumpAndSettle();
318326
}
319327

@@ -581,7 +589,9 @@ void main() {
581589
});
582590

583591
group('error banner', () {
584-
Finder contentFieldFinder() => find.descendant(
592+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
593+
594+
Finder inputFieldFinder() => find.descendant(
585595
of: find.byType(ComposeBox),
586596
matching: find.byType(TextField));
587597

@@ -590,24 +600,26 @@ void main() {
590600
matching: find.widgetWithIcon(IconButton, icon));
591601

592602
void checkComposeBoxParts({required bool areShown}) {
593-
check(contentFieldFinder().evaluate().length).equals(areShown ? 1 : 0);
603+
final inputFieldCount = inputFieldFinder().evaluate().length;
604+
areShown ? check(inputFieldCount).isGreaterThan(0) : check(inputFieldCount).equals(0);
594605
check(attachButtonFinder(Icons.attach_file).evaluate().length).equals(areShown ? 1 : 0);
595606
check(attachButtonFinder(Icons.image).evaluate().length).equals(areShown ? 1 : 0);
596607
check(attachButtonFinder(Icons.camera_alt).evaluate().length).equals(areShown ? 1 : 0);
597608
}
598609

599-
void checkBanner({required bool isShown}) {
600-
final bannerTextFinder = find.text(GlobalLocalizations.zulipLocalizations
601-
.errorBannerDeactivatedDmLabel);
602-
check(bannerTextFinder.evaluate().length).equals(isShown ? 1 : 0);
610+
void checkBannerWithLabel(String label, {required bool isShown}) {
611+
check(find.text(label).evaluate().length).equals(isShown ? 1 : 0);
603612
}
604613

605-
void checkComposeBox({required bool isShown}) {
614+
void checkComposeBoxIsShown(bool isShown, {required String bannerLabel}) {
606615
checkComposeBoxParts(areShown: isShown);
607-
checkBanner(isShown: !isShown);
616+
checkBannerWithLabel(bannerLabel, isShown: !isShown);
608617
}
609618

610619
group('in DMs with deactivated users', () {
620+
void checkComposeBox({required bool isShown}) => checkComposeBoxIsShown(isShown,
621+
bannerLabel: zulipLocalizations.errorBannerDeactivatedDmLabel);
622+
611623
Future<void> changeUserStatus(WidgetTester tester,
612624
{required User user, required bool isActive}) async {
613625
await store.handleEvent(RealmUserUpdateEvent(id: 1,
@@ -686,5 +698,103 @@ void main() {
686698
});
687699
});
688700
});
701+
702+
group('in channel/topic narrow according to channel post policy', () {
703+
void checkComposeBox({required bool isShown}) => checkComposeBoxIsShown(isShown,
704+
bannerLabel: zulipLocalizations.errorBannerCannotPostInChannelLabel);
705+
706+
final narrowTestCases = [
707+
('channel', const ChannelNarrow(1)),
708+
('topic', const TopicNarrow(1, 'topic')),
709+
];
710+
711+
for (final (String narrowType, Narrow narrow) in narrowTestCases) {
712+
testWidgets('compose box is shown in $narrowType narrow', (tester) async {
713+
await prepareComposeBox(tester,
714+
narrow: narrow,
715+
selfUser: eg.user(role: UserRole.administrator),
716+
streams: [eg.stream(streamId: 1,
717+
channelPostPolicy: ChannelPostPolicy.moderators)]);
718+
checkComposeBox(isShown: true);
719+
});
720+
721+
testWidgets('error banner is shown in $narrowType narrow', (tester) async {
722+
await prepareComposeBox(tester,
723+
narrow: narrow,
724+
selfUser: eg.user(role: UserRole.moderator),
725+
streams: [eg.stream(streamId: 1,
726+
channelPostPolicy: ChannelPostPolicy.administrators)]);
727+
checkComposeBox(isShown: false);
728+
});
729+
}
730+
731+
Future<void> changeUserRole(WidgetTester tester, {
732+
required User user,
733+
required UserRole role,
734+
}) async {
735+
await store.handleEvent(RealmUserUpdateEvent(id: 1,
736+
userId: user.userId, role: role));
737+
await tester.pump();
738+
}
739+
740+
Future<void> changeChannelPolicy(WidgetTester tester, {
741+
required ZulipStream channel,
742+
required ChannelPostPolicy policy,
743+
}) async {
744+
await store.handleEvent(eg.channelUpdateEvent(channel,
745+
property: ChannelPropertyName.channelPostPolicy, value: policy));
746+
await tester.pump();
747+
}
748+
749+
testWidgets('user role decreases -> compose box is replaced with the banner', (tester) async {
750+
final selfUser = eg.user(role: UserRole.administrator);
751+
await prepareComposeBox(tester,
752+
narrow: const ChannelNarrow(1),
753+
selfUser: selfUser,
754+
streams: [eg.stream(streamId: 1, channelPostPolicy: ChannelPostPolicy.administrators)]);
755+
checkComposeBox(isShown: true);
756+
757+
await changeUserRole(tester, user: selfUser, role: UserRole.moderator);
758+
checkComposeBox(isShown: false);
759+
});
760+
761+
testWidgets('user role increases -> banner is replaced with the compose box', (tester) async {
762+
final selfUser = eg.user(role: UserRole.guest);
763+
await prepareComposeBox(tester,
764+
narrow: const ChannelNarrow(1),
765+
selfUser: selfUser,
766+
streams: [eg.stream(streamId: 1, channelPostPolicy: ChannelPostPolicy.moderators)]);
767+
checkComposeBox(isShown: false);
768+
769+
await changeUserRole(tester, user: selfUser, role: UserRole.administrator);
770+
checkComposeBox(isShown: true);
771+
});
772+
773+
testWidgets('channel policy becomes stricter -> compose box is replaced with the banner', (tester) async {
774+
final selfUser = eg.user(role: UserRole.guest);
775+
final channel = eg.stream(streamId: 1, channelPostPolicy: ChannelPostPolicy.any);
776+
await prepareComposeBox(tester,
777+
narrow: const ChannelNarrow(1),
778+
selfUser: selfUser,
779+
streams: [channel]);
780+
checkComposeBox(isShown: true);
781+
782+
await changeChannelPolicy(tester, channel: channel, policy: ChannelPostPolicy.fullMembers);
783+
checkComposeBox(isShown: false);
784+
});
785+
786+
testWidgets('channel policy becomes less strict -> banner is replaced with the compose box', (tester) async {
787+
final selfUser = eg.user(role: UserRole.moderator);
788+
final channel = eg.stream(streamId: 1, channelPostPolicy: ChannelPostPolicy.administrators);
789+
await prepareComposeBox(tester,
790+
narrow: const ChannelNarrow(1),
791+
selfUser: selfUser,
792+
streams: [channel]);
793+
checkComposeBox(isShown: false);
794+
795+
await changeChannelPolicy(tester, channel: channel, policy: ChannelPostPolicy.moderators);
796+
checkComposeBox(isShown: true);
797+
});
798+
});
689799
});
690800
}

0 commit comments

Comments
 (0)