Skip to content

Better management of archived channel names #1344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@
"@unknownChannelName": {
"description": "Replacement name for channel when it cannot be found in the store."
},
"channelArchivedLabel": "(archived)",
"@channelArchivedLabel": {
"description": "Label shown next to an archived channel's name in headers."
},
"composeBoxTopicHintText": "Topic",
"@composeBoxTopicHintText": {
"description": "Hint text for topic input widget in compose box."
Expand Down
4 changes: 4 additions & 0 deletions lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ class ZulipStream {

final int streamId;
String name;
@JsonKey(defaultValue: false) // TODO(server-10): remove default
bool isArchived;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make this optional. On older servers, this field will not be sent.

is_archived: boolean
A boolean indicating whether the channel is archived.
Changes: New in Zulip 10.0 (feature level 315). Previously, this endpoint never returned archived channels.

A clean way to do this might be defaulting this to false, since its value can be indicated from the fact that no archived channels were returned from this endpoint before, with a // TODO(server-10) remove default comment.

String description;
String renderedDescription;

Expand All @@ -350,6 +352,7 @@ class ZulipStream {
ZulipStream({
required this.streamId,
required this.name,
required this.isArchived,
required this.description,
required this.renderedDescription,
required this.dateCreated,
Expand Down Expand Up @@ -460,6 +463,7 @@ class Subscription extends ZulipStream {
Subscription({
required super.streamId,
required super.name,
required super.isArchived,
required super.description,
required super.renderedDescription,
required super.dateCreated,
Expand Down
4 changes: 4 additions & 0 deletions lib/api/model/model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/api/route/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Future<InitialSnapshot> registerQueue(ApiConnection connection) {
'user_avatar_url_field_optional': false, // TODO(#254): turn on
'stream_typing_notifications': true,
'user_settings_object': true,
'archived_channels': true,
},
});
}
Expand Down
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,12 @@ abstract class ZulipLocalizations {
/// **'(unknown channel)'**
String get unknownChannelName;

/// Label shown next to an archived channel's name in headers.
///
/// In en, this message translates to:
/// **'(archived)'**
String get channelArchivedLabel;

/// Hint text for topic input widget in compose box.
///
/// In en, this message translates to:
Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get unknownChannelName => '(unknown channel)';

@override
String get channelArchivedLabel => '(archived)';

@override
String get composeBoxTopicHintText => 'Topic';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get unknownChannelName => '(unknown channel)';

@override
String get channelArchivedLabel => '(archived)';

@override
String get composeBoxTopicHintText => 'Topic';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get unknownChannelName => '(unknown channel)';

@override
String get channelArchivedLabel => '(archived)';

@override
String get composeBoxTopicHintText => 'Topic';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_nb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
@override
String get unknownChannelName => '(unknown channel)';

@override
String get channelArchivedLabel => '(archived)';

@override
String get composeBoxTopicHintText => 'Topic';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get unknownChannelName => '(nieznany kanał)';

@override
String get channelArchivedLabel => '(archived)';

@override
String get composeBoxTopicHintText => 'Wątek';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get unknownChannelName => '(unknown channel)';

@override
String get channelArchivedLabel => '(archived)';

@override
String get composeBoxTopicHintText => 'Тема';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_sk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
@override
String get unknownChannelName => '(unknown channel)';

@override
String get channelArchivedLabel => '(archived)';

@override
String get composeBoxTopicHintText => 'Topic';

Expand Down
17 changes: 13 additions & 4 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,22 @@ void showTopicActionSheet(BuildContext context, {
final pageContext = PageRoot.contextOf(context);

final store = PerAccountStoreWidget.of(pageContext);
final channel = store.streams[channelId];
final subscription = store.subscriptions[channelId];

final optionButtons = <ActionSheetMenuItemButton>[];

final isChannelArchived = channel?.isArchived == true;
// TODO(server-7): simplify this condition away
final supportsUnmutingTopics = store.zulipFeatureLevel >= 170;
// TODO(server-8): simplify this condition away
final supportsFollowingTopics = store.zulipFeatureLevel >= 219;

final visibilityOptions = <UserTopicVisibilityPolicy>[];
final visibilityPolicy = store.topicVisibilityPolicy(channelId, topic);
if (subscription == null) {
// Not subscribed to the channel; there is no user topic change to be made.
if (subscription == null || isChannelArchived) {
// Not subscribed to the channel or the channel is archived;
// there is no user topic change to be made.
} else if (!subscription.isMuted) {
// Channel is subscribed and not muted.
switch (visibilityPolicy) {
Expand Down Expand Up @@ -306,7 +309,8 @@ void showTopicActionSheet(BuildContext context, {
// limit for editing topics).
if (someMessageIdInTopic != null
// ignore: unnecessary_null_comparison // null topic names soon to be enabled
&& topic.displayName != null) {
&& topic.displayName != null
&& !isChannelArchived) {
optionButtons.add(ResolveUnresolveButton(pageContext: pageContext,
topic: topic,
someMessageIdInTopic: someMessageIdInTopic));
Expand Down Expand Up @@ -564,14 +568,19 @@ void showMessageActionSheet({required BuildContext context, required Message mes
final messageListPage = MessageListPage.ancestorOf(pageContext);
final isComposeBoxOffered = messageListPage.composeBoxController != null;

bool isInArchivedChannel = false;
if (message is StreamMessage) {
final channel = store.streams[message.streamId];
isInArchivedChannel = channel?.isArchived == true;
}
final isMessageRead = message.flags.contains(MessageFlag.read);
final markAsUnreadSupported = store.zulipFeatureLevel >= 155; // TODO(server-6)
final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead;

final optionButtons = [
ReactionButtons(message: message, pageContext: pageContext),
StarButton(message: message, pageContext: pageContext),
if (isComposeBoxOffered)
if (isComposeBoxOffered && !isInArchivedChannel)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the direct reason that QuoteAndReplyButton can't be offered is that there is no compose box. The channel being archived contributes to that result, but it's not the direct cause. Does leaving this condition and the lookup out give us the same result?

QuoteAndReplyButton(message: message, pageContext: pageContext),
if (showMarkAsUnreadButton)
MarkAsUnreadButton(message: message, pageContext: pageContext),
Expand Down
6 changes: 4 additions & 2 deletions lib/widgets/compose_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1545,8 +1545,10 @@ class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateM
case ChannelNarrow(:final streamId):
case TopicNarrow(:final streamId):
final channel = store.streams[streamId];
if (channel == null || !store.hasPostingPermission(inChannel: channel,
user: store.selfUser, byDate: DateTime.now())) {
if (channel == null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: indentation

|| !store.hasPostingPermission(inChannel: channel,
user: store.selfUser, byDate: DateTime.now())
|| channel.isArchived) {
return _ErrorBanner(getLabel: (zulipLocalizations) =>
zulipLocalizations.errorBannerCannotPostInChannelLabel);
}
Expand Down
51 changes: 39 additions & 12 deletions lib/widgets/inbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ abstract class _HeaderItem extends StatelessWidget {

String title(ZulipLocalizations zulipLocalizations);
IconData get icon;
InlineSpan? buildTrailing(BuildContext context) => null;
Color collapsedIconColor(BuildContext context);
Color uncollapsedIconColor(BuildContext context);
Color uncollapsedBackgroundColor(BuildContext context);
Expand Down Expand Up @@ -285,18 +286,24 @@ abstract class _HeaderItem extends StatelessWidget {
: uncollapsedIconColor(context),
icon),
const SizedBox(width: 5),
Expanded(child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
style: TextStyle(
fontSize: 17,
height: (20 / 17),
// TODO(design) check if this is the right variable
color: designVariables.labelMenuButton,
).merge(weightVariableTextStyle(context, wght: 600)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
title(zulipLocalizations)))),
Expanded(
child: Padding(
Comment on lines +289 to +290
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's preserve the original formatting, to de-indent the following lines

padding: const EdgeInsets.symmetric(vertical: 4),
child: RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
children: [
TextSpan(
text: title(zulipLocalizations),
style: TextStyle(
fontSize: 17,
height: 20 / 17,
// TODO(design) check if this is the right variable
color: designVariables.labelMenuButton)
.merge(weightVariableTextStyle(context, wght: 600))),
buildTrailing(context) ?? const TextSpan(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we can skip the const TextSpan() by using if here:

Suggested change
buildTrailing(context) ?? const TextSpan(),
if (trailing != null) trailing,

where trailing is a local variable storing the result of buildTrailing(context).

])))),
const SizedBox(width: 12),
if (hasMention) const _IconMarker(icon: ZulipIcons.at_sign),
Padding(padding: const EdgeInsetsDirectional.only(end: 16),
Expand Down Expand Up @@ -436,9 +443,11 @@ mixin _LongPressable on _HeaderItem {

class _StreamHeaderItem extends _HeaderItem with _LongPressable {
final Subscription subscription;
final bool isArchived;

const _StreamHeaderItem({
required this.subscription,
required this.isArchived,
required super.collapsed,
required super.pageState,
required super.count,
Expand All @@ -449,6 +458,23 @@ class _StreamHeaderItem extends _HeaderItem with _LongPressable {
@override String title(ZulipLocalizations zulipLocalizations) =>
subscription.name;
@override IconData get icon => iconDataForStream(subscription);
@override InlineSpan? buildTrailing(BuildContext context) {
if (!isArchived) return null;

final designVariables = DesignVariables.of(context);
final zulipLocalizations = ZulipLocalizations.of(context);

return WidgetSpan(
child: Padding(
padding: const EdgeInsetsDirectional.only(start: 4),
child: Text(
zulipLocalizations.channelArchivedLabel,
style: TextStyle(
fontSize: 17,
height: 20 / 17,
color: designVariables.labelMessageHeaderArchived,
fontStyle: FontStyle.italic))));
}
@override Color collapsedIconColor(context) =>
colorSwatchFor(context, subscription).iconOnPlainBackground;
@override Color uncollapsedIconColor(context) =>
Expand Down Expand Up @@ -495,6 +521,7 @@ class _StreamSection extends StatelessWidget {
collapsed: collapsed,
pageState: pageState,
sectionContext: context,
isArchived: subscription.isArchived,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this should match the constructor order of parameters

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, since the data comes from subscription, which is also available to _StreamHeaderItem, we can remove isArchive and access its value from subscription.

);
return StickyHeaderItem(
header: header,
Expand Down
32 changes: 30 additions & 2 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ class MessageListAppBarTitle extends StatelessWidget {
ZulipStream? stream,
}) {
final zulipLocalizations = ZulipLocalizations.of(context);
final designVariables = DesignVariables.of(context);
// A null [Icon.icon] makes a blank space.
final icon = stream != null ? iconDataForStream(stream) : null;
return Row(
Expand All @@ -326,6 +327,17 @@ class MessageListAppBarTitle extends StatelessWidget {
const SizedBox(width: 4),
Flexible(child: Text(
stream?.name ?? zulipLocalizations.unknownChannelName)),
if (stream?.isArchived ?? false)
// TODO(#1285): Avoid concatenating translated strings
Padding(
padding: EdgeInsetsDirectional.fromSTEB(4, 4, 0, 4),
child: Text(
zulipLocalizations.channelArchivedLabel,
style: TextStyle(
fontSize: 18,
// TODO(design): check if this is the right variable
color: designVariables.labelMessageHeaderArchived,
fontStyle: FontStyle.italic))),
]);
}

Expand Down Expand Up @@ -1102,6 +1114,18 @@ class StreamMessageRecipientHeader extends StatelessWidget {
style: recipientHeaderTextStyle(context),
overflow: TextOverflow.ellipsis),
),
if (stream?.isArchived ?? false)
// TODO(#1285): Avoid concatenating translated strings
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(4, 4, 0, 4),
child: Text(
zulipLocalizations.channelArchivedLabel,
style: recipientHeaderTextStyle(context,
// TODO(design): check if this is the right variable
color: designVariables.labelMessageHeaderArchived,
fontStyle: FontStyle.italic),
overflow: TextOverflow.ellipsis,
maxLines: 1)),
Comment on lines +1127 to +1128
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While testing this manually, these don't seem to have an effect. From what I can see, the recipient header starts to overflow before we would truncate the "(archived)" label.

image

Maybe it is fine to not have these options specified.

Padding(
// Figma has 5px horizontal padding around an 8px wide icon.
// Icon is 16px wide here so horizontal padding is 1px.
Expand Down Expand Up @@ -1220,9 +1244,13 @@ class DmRecipientHeader extends StatelessWidget {
}
}

TextStyle recipientHeaderTextStyle(BuildContext context, {FontStyle? fontStyle}) {
TextStyle recipientHeaderTextStyle(
BuildContext context, {
Color? color,
FontStyle? fontStyle,
}) {
return TextStyle(
color: DesignVariables.of(context).title,
color: color ?? DesignVariables.of(context).title,
fontSize: 16,
letterSpacing: proportionalLetterSpacing(context, 0.02, baseFontSize: 16),
height: (18 / 16),
Expand Down
3 changes: 3 additions & 0 deletions lib/widgets/subscription_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class _SubscriptionListPageBodyState extends State<SubscriptionListPageBody> wit
final List<Subscription> pinned = [];
final List<Subscription> unpinned = [];
for (final subscription in store.subscriptions.values) {
if (subscription.isArchived) continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure that _SubscriptionList checks for this with an assertion in its build method.

if (subscription.pinToTop) {
pinned.add(subscription);
} else {
Expand Down Expand Up @@ -188,6 +189,8 @@ class _SubscriptionList extends StatelessWidget {

@override
Widget build(BuildContext context) {
assert(subscriptions.every((subscription) => !subscription.isArchived));

return SliverList.builder(
itemCount: subscriptions.length,
itemBuilder: (BuildContext context, int index) {
Expand Down
Loading