Skip to content

Commit 2600b1c

Browse files
committed
compose_box: Replace compose box with a banner when cannot post in a channel
Fixes: #674
1 parent 08e02eb commit 2600b1c

File tree

3 files changed

+154
-30
lines changed

3 files changed

+154
-30
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: 119 additions & 12 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 account = eg.account(user: selfUser);
56+
await testBinding.globalStore.add(account, eg.initialSnapshot(
57+
realmWaitingPeriodThreshold: realmWaitingPeriodThreshold));
5358

54-
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
59+
store = await testBinding.globalStore.perAccount(account.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: account.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.
@@ -579,7 +584,9 @@ void main() {
579584
});
580585

581586
group('error banner', () {
582-
Finder contentFieldFinder() => find.descendant(
587+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
588+
589+
Finder inputFieldFinder() => find.descendant(
583590
of: find.byType(ComposeBox),
584591
matching: find.byType(TextField));
585592

@@ -588,24 +595,26 @@ void main() {
588595
matching: find.widgetWithIcon(IconButton, icon));
589596

590597
void checkComposeBoxParts({required bool areShown}) {
591-
check(contentFieldFinder().evaluate().length).equals(areShown ? 1 : 0);
598+
final inputFieldCount = inputFieldFinder().evaluate().length;
599+
areShown ? check(inputFieldCount).isGreaterThan(0) : check(inputFieldCount).equals(0);
592600
check(attachButtonFinder(Icons.attach_file).evaluate().length).equals(areShown ? 1 : 0);
593601
check(attachButtonFinder(Icons.image).evaluate().length).equals(areShown ? 1 : 0);
594602
check(attachButtonFinder(Icons.camera_alt).evaluate().length).equals(areShown ? 1 : 0);
595603
}
596604

597-
void checkBanner({required bool isShown}) {
598-
final bannerTextFinder = find.text(GlobalLocalizations.zulipLocalizations
599-
.errorBannerDeactivatedDmLabel);
600-
check(bannerTextFinder.evaluate().length).equals(isShown ? 1 : 0);
605+
void checkBannerWithLabel(String label, {required bool isShown}) {
606+
check(find.text(label).evaluate().length).equals(isShown ? 1 : 0);
601607
}
602608

603-
void checkComposeBox({required bool isShown}) {
609+
void checkComposeBoxIsShown(bool isShown, {required String bannerLabel}) {
604610
checkComposeBoxParts(areShown: isShown);
605-
checkBanner(isShown: !isShown);
611+
checkBannerWithLabel(bannerLabel, isShown: !isShown);
606612
}
607613

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

0 commit comments

Comments
 (0)