Skip to content
Merged
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
65 changes: 65 additions & 0 deletions lib/model/server_support.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import '../api/core.dart';
import '../api/exception.dart';
import '../api/model/initial_snapshot.dart';
import '../api/route/realm.dart';
Copy link
Member

Choose a reason for hiding this comment

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

nit: third commit removes this line; second commit could omit adding it

import 'database.dart';

/// The fields 'zulip_version', 'zulip_merge_base', and 'zulip_feature_level'
/// from a /server_settings or /register response.
class ZulipVersionData {
const ZulipVersionData({
required this.zulipVersion,
required this.zulipMergeBase,
required this.zulipFeatureLevel,
});

factory ZulipVersionData.fromServerSettings(GetServerSettingsResult serverSettings) =>
ZulipVersionData(
zulipVersion: serverSettings.zulipVersion,
zulipMergeBase: serverSettings.zulipMergeBase,
zulipFeatureLevel: serverSettings.zulipFeatureLevel);

factory ZulipVersionData.fromInitialSnapshot(InitialSnapshot initialSnapshot) =>
ZulipVersionData(
zulipVersion: initialSnapshot.zulipVersion,
zulipMergeBase: initialSnapshot.zulipMergeBase,
zulipFeatureLevel: initialSnapshot.zulipFeatureLevel);

/// Make a [ZulipVersionData] from a [MalformedServerResponseException],
/// if the body was readable/valid JSON and contained the data, else null.
///
/// May be used for the /server_settings or the /register response.
///
/// If there's a zulip_version but no zulip_feature_level,
/// we infer it's indeed a Zulip server,
/// just an ancient one before feature levels were introduced in Zulip 3.0,
/// and we set 0 for zulipFeatureLevel.
static ZulipVersionData? fromMalformedServerResponseException(MalformedServerResponseException e) {
try {
final data = e.data!;
return ZulipVersionData(
zulipVersion: data['zulip_version'] as String,
zulipMergeBase: data['zulip_merge_base'] as String?,
zulipFeatureLevel: data['zulip_feature_level'] as int? ?? 0);
} catch (inner) {
return null;
}
}

final String zulipVersion;
final String? zulipMergeBase;
final int zulipFeatureLevel;

bool matchesAccount(Account account) =>
zulipVersion == account.zulipVersion
&& zulipMergeBase == account.zulipMergeBase
&& zulipFeatureLevel == account.zulipFeatureLevel;

bool get isUnsupported => zulipFeatureLevel < kMinSupportedZulipFeatureLevel;
}

class ServerVersionUnsupportedException implements Exception {
final ZulipVersionData data;

ServerVersionUnsupportedException(this.data);
}
63 changes: 6 additions & 57 deletions lib/model/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import 'presence.dart';
import 'realm.dart';
import 'recent_dm_conversations.dart';
import 'recent_senders.dart';
import 'server_support.dart';
import 'channel.dart';
import 'saved_snippet.dart';
import 'settings.dart';
Expand Down Expand Up @@ -208,7 +209,7 @@ abstract class GlobalStore extends ChangeNotifier {
assert(account != null); // doLoadPerAccount would have thrown AccountNotFoundException
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
switch (e) {
case _ServerVersionUnsupportedException():
case ServerVersionUnsupportedException():
reportErrorToUserModally(
zulipLocalizations.errorCouldNotConnectTitle,
message: zulipLocalizations.errorServerVersionUnsupportedMessage(
Expand Down Expand Up @@ -1026,9 +1027,9 @@ class UpdateMachine {
try {
initialSnapshot = await _registerQueueWithRetry(connection,
stopAndThrowIfNoAccount: stopAndThrowIfNoAccount);
} on _ServerVersionUnsupportedException catch (e) {
} on ServerVersionUnsupportedException catch (e) {
// `!` is OK because _registerQueueWithRetry would have thrown a
// not-_ServerVersionUnsupportedException if no account
// not-ServerVersionUnsupportedException if no account
final account = globalStore.getAccount(accountId)!;
if (!e.data.matchesAccount(account)) {
await globalStore.updateZulipVersionData(accountId, e.data);
Expand Down Expand Up @@ -1094,7 +1095,7 @@ class UpdateMachine {
case MalformedServerResponseException()
when (zulipVersionData = ZulipVersionData.fromMalformedServerResponseException(e))
?.isUnsupported == true:
throw _ServerVersionUnsupportedException(zulipVersionData!);
throw ServerVersionUnsupportedException(zulipVersionData!);
case HttpException(httpStatus: 401):
// We cannot recover from this error through retrying.
// Leave it to [GlobalStore.loadPerAccount].
Expand All @@ -1114,7 +1115,7 @@ class UpdateMachine {
stopAndThrowIfNoAccount();
final zulipVersionData = ZulipVersionData.fromInitialSnapshot(result);
if (zulipVersionData.isUnsupported) {
throw _ServerVersionUnsupportedException(zulipVersionData);
throw ServerVersionUnsupportedException(zulipVersionData);
}
return result;
}
Expand Down Expand Up @@ -1529,58 +1530,6 @@ class UpdateMachine {
String toString() => '${objectRuntimeType(this, 'UpdateMachine')}#${shortHash(this)}';
}

/// The fields 'zulip_version', 'zulip_merge_base', and 'zulip_feature_level'
/// from a /register response.
class ZulipVersionData {
const ZulipVersionData({
required this.zulipVersion,
required this.zulipMergeBase,
required this.zulipFeatureLevel,
});

factory ZulipVersionData.fromInitialSnapshot(InitialSnapshot initialSnapshot) =>
ZulipVersionData(
zulipVersion: initialSnapshot.zulipVersion,
zulipMergeBase: initialSnapshot.zulipMergeBase,
zulipFeatureLevel: initialSnapshot.zulipFeatureLevel);

/// Make a [ZulipVersionData] from a [MalformedServerResponseException],
/// if the body was readable/valid JSON and contained the data, else null.
///
/// If there's a zulip_version but no zulip_feature_level,
/// we infer it's indeed a Zulip server,
/// just an ancient one before feature levels were introduced in Zulip 3.0,
/// and we set 0 for zulipFeatureLevel.
static ZulipVersionData? fromMalformedServerResponseException(MalformedServerResponseException e) {
try {
final data = e.data!;
return ZulipVersionData(
zulipVersion: data['zulip_version'] as String,
zulipMergeBase: data['zulip_merge_base'] as String?,
zulipFeatureLevel: data['zulip_feature_level'] as int? ?? 0);
} catch (inner) {
return null;
}
}

final String zulipVersion;
final String? zulipMergeBase;
final int zulipFeatureLevel;

bool matchesAccount(Account account) =>
zulipVersion == account.zulipVersion
&& zulipMergeBase == account.zulipMergeBase
&& zulipFeatureLevel == account.zulipFeatureLevel;

bool get isUnsupported => zulipFeatureLevel < kMinSupportedZulipFeatureLevel;
}

class _ServerVersionUnsupportedException implements Exception {
final ZulipVersionData data;

_ServerVersionUnsupportedException(this.data);
}

class _EventHandlingException implements Exception {
final Object cause;
final Event event;
Expand Down
40 changes: 30 additions & 10 deletions lib/widgets/login.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';

import '../api/core.dart';
import '../api/exception.dart';
import '../api/model/web_auth.dart';
import '../api/route/account.dart';
Expand All @@ -13,6 +14,7 @@ import '../api/route/users.dart';
import '../generated/l10n/zulip_localizations.dart';
import '../log.dart';
import '../model/binding.dart';
import '../model/server_support.dart';
import '../model/store.dart';
import 'dialog.dart';
import 'home.dart';
Expand Down Expand Up @@ -180,25 +182,43 @@ class _AddAccountPageState extends State<AddAccountPage> {
final connection = globalStore.apiConnection(realmUrl: url!, zulipFeatureLevel: null);
try {
serverSettings = await getServerSettings(connection);
final zulipVersionData = ZulipVersionData.fromServerSettings(serverSettings);
if (zulipVersionData.isUnsupported) {
throw ServerVersionUnsupportedException(zulipVersionData);
}
} on MalformedServerResponseException catch (e) {
final zulipVersionData = ZulipVersionData.fromMalformedServerResponseException(e);
if (zulipVersionData != null && zulipVersionData.isUnsupported) {
throw ServerVersionUnsupportedException(zulipVersionData);
}
rethrow;
} finally {
connection.close();
}
} catch (e) {
if (!context.mounted) {
return;
if (!context.mounted) return;

String? message;
Uri? learnMoreButtonUrl;
switch (e) {
case ServerVersionUnsupportedException(:final data):
message = zulipLocalizations.errorServerVersionUnsupportedMessage(
url.toString(),
data.zulipVersion,
kMinSupportedZulipVersion);
learnMoreButtonUrl = kServerSupportDocUrl;
default:
// TODO(#105) give more helpful feedback; see `fetchServerSettings`
// in zulip-mobile's src/message/fetchActions.js.
message = zulipLocalizations.errorLoginCouldNotConnect(url.toString());
}
// TODO(#105) give more helpful feedback; see `fetchServerSettings`
// in zulip-mobile's src/message/fetchActions.js.
showErrorDialog(context: context,
title: zulipLocalizations.errorCouldNotConnectTitle,
message: zulipLocalizations.errorLoginCouldNotConnect(url.toString()));
return;
}
// https://github.com/dart-lang/linter/issues/4007
// ignore: use_build_context_synchronously
if (!context.mounted) {
message: message,
learnMoreButtonUrl: learnMoreButtonUrl);
return;
}
if (!context.mounted) return;

unawaited(Navigator.push(context,
LoginPage.buildRoute(serverSettings: serverSettings)));
Expand Down
6 changes: 6 additions & 0 deletions test/api/route/route_checks.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import 'package:checks/checks.dart';
import 'package:zulip/api/route/messages.dart';
import 'package:zulip/api/route/realm.dart';
import 'package:zulip/api/route/saved_snippets.dart';

extension SendMessageResultChecks on Subject<SendMessageResult> {
Subject<int> get id => has((e) => e.id, 'id');
}

extension CreateSavedSnippetResultChecks on Subject<CreateSavedSnippetResult> {
Subject<int> get savedSnippetId => has((e) => e.savedSnippetId, 'savedSnippetId');
}

extension GetServerSettingsResultChecks on Subject<GetServerSettingsResult> {
Subject<Uri> get realmUrl => has((e) => e.realmUrl, 'realmUrl');
}

// TODO add similar extensions for other classes in api/route/*.dart
1 change: 1 addition & 0 deletions test/model/store_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:zulip/model/autocomplete.dart';
import 'package:zulip/model/binding.dart';
import 'package:zulip/model/database.dart';
import 'package:zulip/model/recent_dm_conversations.dart';
import 'package:zulip/model/server_support.dart';
import 'package:zulip/model/settings.dart';
import 'package:zulip/model/store.dart';
import 'package:zulip/model/unreads.dart';
Expand Down
1 change: 1 addition & 0 deletions test/model/store_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'package:zulip/api/route/realm.dart';
import 'package:zulip/log.dart';
import 'package:zulip/model/actions.dart';
import 'package:zulip/model/presence.dart';
import 'package:zulip/model/server_support.dart';
import 'package:zulip/model/store.dart';
import 'package:zulip/notifications/receive.dart';

Expand Down
6 changes: 6 additions & 0 deletions test/widgets/checks.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:checks/checks.dart';
import 'package:flutter/widgets.dart';
import 'package:zulip/api/route/realm.dart';

import 'package:zulip/model/emoji.dart';
import 'package:zulip/model/narrow.dart';
Expand All @@ -8,6 +9,7 @@ import 'package:zulip/widgets/compose_box.dart';
import 'package:zulip/widgets/content.dart';
import 'package:zulip/widgets/emoji.dart';
import 'package:zulip/widgets/emoji_reaction.dart';
import 'package:zulip/widgets/login.dart';
import 'package:zulip/widgets/message_list.dart';
import 'package:zulip/widgets/page.dart';
import 'package:zulip/widgets/profile.dart';
Expand Down Expand Up @@ -74,6 +76,10 @@ extension AccountRouteChecks<T> on Subject<AccountRoute<T>> {
Subject<int> get accountId => has((x) => x.accountId, 'accountId');
}

extension LoginPageChecks on Subject<LoginPage> {
Subject<GetServerSettingsResult> get serverSettings => has((x) => x.serverSettings, 'serverSettings');
}

extension ProfilePageChecks on Subject<ProfilePage> {
Subject<int> get userId => has((x) => x.userId, 'userId');
}
Expand Down
1 change: 0 additions & 1 deletion test/widgets/dialog_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:zulip/model/settings.dart';
import 'package:zulip/widgets/app.dart';
import 'package:zulip/widgets/dialog.dart';
import 'package:zulip/widgets/store.dart';
Copy link
Member

Choose a reason for hiding this comment

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

dialog test [nfc]: Remove an unused import

Looks like this slipped through CI, introduced recently in
8d3c1284e.

Ah, oops. I guess that changed in your revision of #1782, and I merged before CI finished running.

… In fact I'm pretty sure I ran flutter test on this very file before merging, because I saw CI wasn't done. But I guess I should have checked the analyzer output too.


import '../model/binding.dart';
import 'dialog_checks.dart';
Expand Down
Loading