From c50a521990a33f413698b6c6c62aff1120b62905 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 30 Apr 2025 14:01:17 -0700 Subject: [PATCH 01/22] deps: Upgrade Flutter to 3.32.0-1.0.pre.332 And update Flutter's supporting libraries to match. In particular this pulls in this recent PR of mine so we can use it: https://github.com/flutter/flutter/pull/166731 This also causes all the generated l10n files to get reformatted. That's due to this upstream PR: https://github.com/flutter/flutter/pull/167029 --- lib/generated/l10n/zulip_localizations.dart | 85 +++++++++---- .../l10n/zulip_localizations_ar.dart | 75 +++++++---- .../l10n/zulip_localizations_en.dart | 75 +++++++---- .../l10n/zulip_localizations_ja.dart | 75 +++++++---- .../l10n/zulip_localizations_nb.dart | 75 +++++++---- .../l10n/zulip_localizations_pl.dart | 108 +++++++++++----- .../l10n/zulip_localizations_ru.dart | 117 ++++++++++++------ .../l10n/zulip_localizations_sk.dart | 81 ++++++++---- .../l10n/zulip_localizations_uk.dart | 117 ++++++++++++------ pubspec.lock | 4 +- pubspec.yaml | 4 +- 11 files changed, 569 insertions(+), 247 deletions(-) diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index e8b15440e3..e326703e4b 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -68,7 +68,8 @@ import 'zulip_localizations_uk.dart'; /// be consistent with the languages listed in the ZulipLocalizations.supportedLocales /// property. abstract class ZulipLocalizations { - ZulipLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + ZulipLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -76,7 +77,8 @@ abstract class ZulipLocalizations { return Localizations.of(context, ZulipLocalizations)!; } - static const LocalizationsDelegate delegate = _ZulipLocalizationsDelegate(); + static const LocalizationsDelegate delegate = + _ZulipLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -88,12 +90,13 @@ abstract class ZulipLocalizations { /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. - static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ @@ -104,7 +107,7 @@ abstract class ZulipLocalizations { Locale('pl'), Locale('ru'), Locale('sk'), - Locale('uk') + Locale('uk'), ]; /// Title for About Zulip page. @@ -381,7 +384,11 @@ abstract class ZulipLocalizations { /// /// In en, this message translates to: /// **'{num, plural, =1{File is} other{{num} files are}} larger than the server\'s limit of {maxFileUploadSizeMib} MiB and will not be uploaded:\n\n{listMessage}'** - String errorFilesTooLarge(int num, int maxFileUploadSizeMib, String listMessage); + String errorFilesTooLarge( + int num, + int maxFileUploadSizeMib, + String listMessage, + ); /// Error title when attached files are too large in size. /// @@ -459,7 +466,11 @@ abstract class ZulipLocalizations { /// /// In en, this message translates to: /// **'Error handling a Zulip event from {serverUrl}; will retry.\n\nError: {error}\n\nEvent: {event}'** - String errorHandlingEventDetails(String serverUrl, String error, String event); + String errorHandlingEventDetails( + String serverUrl, + String error, + String event, + ); /// Error title when opening a link failed. /// @@ -831,7 +842,11 @@ abstract class ZulipLocalizations { /// /// In en, this message translates to: /// **'{url} is running Zulip Server {zulipVersion}, which is unsupported. The minimum supported version is Zulip Server {minSupportedZulipVersion}.'** - String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion); + String errorServerVersionUnsupportedMessage( + String url, + String zulipVersion, + String minSupportedZulipVersion, + ); /// Error message in the dialog for invalid API key. /// @@ -1290,40 +1305,58 @@ abstract class ZulipLocalizations { String get zulipAppTitle; } -class _ZulipLocalizationsDelegate extends LocalizationsDelegate { +class _ZulipLocalizationsDelegate + extends LocalizationsDelegate { const _ZulipLocalizationsDelegate(); @override Future load(Locale locale) { - return SynchronousFuture(lookupZulipLocalizations(locale)); + return SynchronousFuture( + lookupZulipLocalizations(locale), + ); } @override - bool isSupported(Locale locale) => ['ar', 'en', 'ja', 'nb', 'pl', 'ru', 'sk', 'uk'].contains(locale.languageCode); + bool isSupported(Locale locale) => [ + 'ar', + 'en', + 'ja', + 'nb', + 'pl', + 'ru', + 'sk', + 'uk', + ].contains(locale.languageCode); @override bool shouldReload(_ZulipLocalizationsDelegate old) => false; } ZulipLocalizations lookupZulipLocalizations(Locale locale) { - - // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'ar': return ZulipLocalizationsAr(); - case 'en': return ZulipLocalizationsEn(); - case 'ja': return ZulipLocalizationsJa(); - case 'nb': return ZulipLocalizationsNb(); - case 'pl': return ZulipLocalizationsPl(); - case 'ru': return ZulipLocalizationsRu(); - case 'sk': return ZulipLocalizationsSk(); - case 'uk': return ZulipLocalizationsUk(); + case 'ar': + return ZulipLocalizationsAr(); + case 'en': + return ZulipLocalizationsEn(); + case 'ja': + return ZulipLocalizationsJa(); + case 'nb': + return ZulipLocalizationsNb(); + case 'pl': + return ZulipLocalizationsPl(); + case 'ru': + return ZulipLocalizationsRu(); + case 'sk': + return ZulipLocalizationsSk(); + case 'uk': + return ZulipLocalizationsUk(); } throw FlutterError( 'ZulipLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 'an issue with the localizations generation tool. Please file an issue ' 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.' + 'that was used.', ); } diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart index c2478f4613..8d36fa6bd0 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -44,7 +44,8 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get logOutConfirmationDialogTitle => 'Log out?'; @override - String get logOutConfirmationDialogMessage => 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; + String get logOutConfirmationDialogMessage => + 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; @override String get logOutConfirmationDialogConfirmButton => 'Log out'; @@ -65,10 +66,12 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get permissionsNeededOpenSettings => 'Open settings'; @override - String get permissionsDeniedCameraAccess => 'To upload an image, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedCameraAccess => + 'To upload an image, please grant Zulip additional permissions in Settings.'; @override - String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedReadExternalStorage => + 'To upload files, please grant Zulip additional permissions in Settings.'; @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; @@ -95,7 +98,8 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved'; @override - String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved'; + String get errorUnresolveTopicFailedTitle => + 'Failed to mark topic as unresolved'; @override String get actionSheetOptionCopyMessageText => 'Copy message text'; @@ -136,7 +140,8 @@ class ZulipLocalizationsAr extends ZulipLocalizations { } @override - String get errorCouldNotFetchMessageSource => 'Could not fetch message source'; + String get errorCouldNotFetchMessageSource => + 'Could not fetch message source'; @override String get errorCopyingFailed => 'Copying failed'; @@ -152,7 +157,11 @@ class ZulipLocalizationsAr extends ZulipLocalizations { } @override - String errorFilesTooLarge(int num, int maxFileUploadSizeMib, String listMessage) { + String errorFilesTooLarge( + int num, + int maxFileUploadSizeMib, + String listMessage, + ) { String _temp0 = intl.Intl.pluralLogic( num, locale: localeName, @@ -191,7 +200,8 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get errorCouldNotConnectTitle => 'Could not connect'; @override - String get errorMessageDoesNotSeemToExist => 'That message does not seem to exist.'; + String get errorMessageDoesNotSeemToExist => + 'That message does not seem to exist.'; @override String get errorQuotationFailed => 'Quotation failed'; @@ -202,7 +212,8 @@ class ZulipLocalizationsAr extends ZulipLocalizations { } @override - String get errorConnectingToServerShort => 'Error connecting to Zulip. Retrying…'; + String get errorConnectingToServerShort => + 'Error connecting to Zulip. Retrying…'; @override String errorConnectingToServerDetails(String serverUrl, String error) { @@ -210,10 +221,15 @@ class ZulipLocalizationsAr extends ZulipLocalizations { } @override - String get errorHandlingEventTitle => 'Error handling a Zulip event. Retrying connection…'; + String get errorHandlingEventTitle => + 'Error handling a Zulip event. Retrying connection…'; @override - String errorHandlingEventDetails(String serverUrl, String error, String event) { + String errorHandlingEventDetails( + String serverUrl, + String error, + String event, + ) { return 'Error handling a Zulip event from $serverUrl; will retry.\n\nError: $error\n\nEvent: $event'; } @@ -256,10 +272,12 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get successMessageLinkCopied => 'Message link copied'; @override - String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; + String get errorBannerDeactivatedDmLabel => + 'You cannot send messages to deactivated users.'; @override - String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + String get errorBannerCannotPostInChannelLabel => + 'You do not have permission to post in this channel.'; @override String get composeBoxAttachFilesTooltip => 'Attach files'; @@ -328,16 +346,19 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get messageListGroupYouWithYourself => 'Messages with yourself'; @override - String get contentValidationErrorTooLong => 'Message length shouldn\'t be greater than 10000 characters.'; + String get contentValidationErrorTooLong => + 'Message length shouldn\'t be greater than 10000 characters.'; @override String get contentValidationErrorEmpty => 'You have nothing to send!'; @override - String get contentValidationErrorQuoteAndReplyInProgress => 'Please wait for the quotation to complete.'; + String get contentValidationErrorQuoteAndReplyInProgress => + 'Please wait for the quotation to complete.'; @override - String get contentValidationErrorUploadInProgress => 'Please wait for the upload to complete.'; + String get contentValidationErrorUploadInProgress => + 'Please wait for the upload to complete.'; @override String get dialogCancel => 'Cancel'; @@ -411,13 +432,19 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get loginErrorMissingUsername => 'Please enter your username.'; @override - String get topicValidationErrorTooLong => 'Topic length shouldn\'t be greater than 60 characters.'; + String get topicValidationErrorTooLong => + 'Topic length shouldn\'t be greater than 60 characters.'; @override - String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.'; + String get topicValidationErrorMandatoryButEmpty => + 'Topics are required in this organization.'; @override - String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) { + String errorServerVersionUnsupportedMessage( + String url, + String zulipVersion, + String minSupportedZulipVersion, + ) { return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.'; } @@ -457,10 +484,12 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get serverUrlValidationErrorInvalidUrl => 'Please enter a valid URL.'; @override - String get serverUrlValidationErrorNoUseEmail => 'Please enter the server URL, not your email.'; + String get serverUrlValidationErrorNoUseEmail => + 'Please enter the server URL, not your email.'; @override - String get serverUrlValidationErrorUnsupportedScheme => 'The server URL must start with http:// or https://.'; + String get serverUrlValidationErrorUnsupportedScheme => + 'The server URL must start with http:// or https://.'; @override String get spoilerDefaultHeaderText => 'Spoiler'; @@ -655,13 +684,15 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get experimentalFeatureSettingsPageTitle => 'Experimental features'; @override - String get experimentalFeatureSettingsWarning => 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; + String get experimentalFeatureSettingsWarning => + 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; @override String get errorNotificationOpenTitle => 'Failed to open notification'; @override - String get errorNotificationOpenAccountMissing => 'The account associated with this notification no longer exists.'; + String get errorNotificationOpenAccountMissing => + 'The account associated with this notification no longer exists.'; @override String get errorReactionAddingFailedTitle => 'Adding reaction failed'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 289ba33af2..a74a2e1eaf 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -44,7 +44,8 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get logOutConfirmationDialogTitle => 'Log out?'; @override - String get logOutConfirmationDialogMessage => 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; + String get logOutConfirmationDialogMessage => + 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; @override String get logOutConfirmationDialogConfirmButton => 'Log out'; @@ -65,10 +66,12 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get permissionsNeededOpenSettings => 'Open settings'; @override - String get permissionsDeniedCameraAccess => 'To upload an image, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedCameraAccess => + 'To upload an image, please grant Zulip additional permissions in Settings.'; @override - String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedReadExternalStorage => + 'To upload files, please grant Zulip additional permissions in Settings.'; @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; @@ -95,7 +98,8 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved'; @override - String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved'; + String get errorUnresolveTopicFailedTitle => + 'Failed to mark topic as unresolved'; @override String get actionSheetOptionCopyMessageText => 'Copy message text'; @@ -136,7 +140,8 @@ class ZulipLocalizationsEn extends ZulipLocalizations { } @override - String get errorCouldNotFetchMessageSource => 'Could not fetch message source'; + String get errorCouldNotFetchMessageSource => + 'Could not fetch message source'; @override String get errorCopyingFailed => 'Copying failed'; @@ -152,7 +157,11 @@ class ZulipLocalizationsEn extends ZulipLocalizations { } @override - String errorFilesTooLarge(int num, int maxFileUploadSizeMib, String listMessage) { + String errorFilesTooLarge( + int num, + int maxFileUploadSizeMib, + String listMessage, + ) { String _temp0 = intl.Intl.pluralLogic( num, locale: localeName, @@ -191,7 +200,8 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get errorCouldNotConnectTitle => 'Could not connect'; @override - String get errorMessageDoesNotSeemToExist => 'That message does not seem to exist.'; + String get errorMessageDoesNotSeemToExist => + 'That message does not seem to exist.'; @override String get errorQuotationFailed => 'Quotation failed'; @@ -202,7 +212,8 @@ class ZulipLocalizationsEn extends ZulipLocalizations { } @override - String get errorConnectingToServerShort => 'Error connecting to Zulip. Retrying…'; + String get errorConnectingToServerShort => + 'Error connecting to Zulip. Retrying…'; @override String errorConnectingToServerDetails(String serverUrl, String error) { @@ -210,10 +221,15 @@ class ZulipLocalizationsEn extends ZulipLocalizations { } @override - String get errorHandlingEventTitle => 'Error handling a Zulip event. Retrying connection…'; + String get errorHandlingEventTitle => + 'Error handling a Zulip event. Retrying connection…'; @override - String errorHandlingEventDetails(String serverUrl, String error, String event) { + String errorHandlingEventDetails( + String serverUrl, + String error, + String event, + ) { return 'Error handling a Zulip event from $serverUrl; will retry.\n\nError: $error\n\nEvent: $event'; } @@ -256,10 +272,12 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get successMessageLinkCopied => 'Message link copied'; @override - String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; + String get errorBannerDeactivatedDmLabel => + 'You cannot send messages to deactivated users.'; @override - String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + String get errorBannerCannotPostInChannelLabel => + 'You do not have permission to post in this channel.'; @override String get composeBoxAttachFilesTooltip => 'Attach files'; @@ -328,16 +346,19 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get messageListGroupYouWithYourself => 'Messages with yourself'; @override - String get contentValidationErrorTooLong => 'Message length shouldn\'t be greater than 10000 characters.'; + String get contentValidationErrorTooLong => + 'Message length shouldn\'t be greater than 10000 characters.'; @override String get contentValidationErrorEmpty => 'You have nothing to send!'; @override - String get contentValidationErrorQuoteAndReplyInProgress => 'Please wait for the quotation to complete.'; + String get contentValidationErrorQuoteAndReplyInProgress => + 'Please wait for the quotation to complete.'; @override - String get contentValidationErrorUploadInProgress => 'Please wait for the upload to complete.'; + String get contentValidationErrorUploadInProgress => + 'Please wait for the upload to complete.'; @override String get dialogCancel => 'Cancel'; @@ -411,13 +432,19 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get loginErrorMissingUsername => 'Please enter your username.'; @override - String get topicValidationErrorTooLong => 'Topic length shouldn\'t be greater than 60 characters.'; + String get topicValidationErrorTooLong => + 'Topic length shouldn\'t be greater than 60 characters.'; @override - String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.'; + String get topicValidationErrorMandatoryButEmpty => + 'Topics are required in this organization.'; @override - String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) { + String errorServerVersionUnsupportedMessage( + String url, + String zulipVersion, + String minSupportedZulipVersion, + ) { return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.'; } @@ -457,10 +484,12 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get serverUrlValidationErrorInvalidUrl => 'Please enter a valid URL.'; @override - String get serverUrlValidationErrorNoUseEmail => 'Please enter the server URL, not your email.'; + String get serverUrlValidationErrorNoUseEmail => + 'Please enter the server URL, not your email.'; @override - String get serverUrlValidationErrorUnsupportedScheme => 'The server URL must start with http:// or https://.'; + String get serverUrlValidationErrorUnsupportedScheme => + 'The server URL must start with http:// or https://.'; @override String get spoilerDefaultHeaderText => 'Spoiler'; @@ -655,13 +684,15 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get experimentalFeatureSettingsPageTitle => 'Experimental features'; @override - String get experimentalFeatureSettingsWarning => 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; + String get experimentalFeatureSettingsWarning => + 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; @override String get errorNotificationOpenTitle => 'Failed to open notification'; @override - String get errorNotificationOpenAccountMissing => 'The account associated with this notification no longer exists.'; + String get errorNotificationOpenAccountMissing => + 'The account associated with this notification no longer exists.'; @override String get errorReactionAddingFailedTitle => 'Adding reaction failed'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index 00537f73a2..c11a3fae23 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -44,7 +44,8 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get logOutConfirmationDialogTitle => 'Log out?'; @override - String get logOutConfirmationDialogMessage => 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; + String get logOutConfirmationDialogMessage => + 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; @override String get logOutConfirmationDialogConfirmButton => 'Log out'; @@ -65,10 +66,12 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get permissionsNeededOpenSettings => 'Open settings'; @override - String get permissionsDeniedCameraAccess => 'To upload an image, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedCameraAccess => + 'To upload an image, please grant Zulip additional permissions in Settings.'; @override - String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedReadExternalStorage => + 'To upload files, please grant Zulip additional permissions in Settings.'; @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; @@ -95,7 +98,8 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved'; @override - String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved'; + String get errorUnresolveTopicFailedTitle => + 'Failed to mark topic as unresolved'; @override String get actionSheetOptionCopyMessageText => 'Copy message text'; @@ -136,7 +140,8 @@ class ZulipLocalizationsJa extends ZulipLocalizations { } @override - String get errorCouldNotFetchMessageSource => 'Could not fetch message source'; + String get errorCouldNotFetchMessageSource => + 'Could not fetch message source'; @override String get errorCopyingFailed => 'Copying failed'; @@ -152,7 +157,11 @@ class ZulipLocalizationsJa extends ZulipLocalizations { } @override - String errorFilesTooLarge(int num, int maxFileUploadSizeMib, String listMessage) { + String errorFilesTooLarge( + int num, + int maxFileUploadSizeMib, + String listMessage, + ) { String _temp0 = intl.Intl.pluralLogic( num, locale: localeName, @@ -191,7 +200,8 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get errorCouldNotConnectTitle => 'Could not connect'; @override - String get errorMessageDoesNotSeemToExist => 'That message does not seem to exist.'; + String get errorMessageDoesNotSeemToExist => + 'That message does not seem to exist.'; @override String get errorQuotationFailed => 'Quotation failed'; @@ -202,7 +212,8 @@ class ZulipLocalizationsJa extends ZulipLocalizations { } @override - String get errorConnectingToServerShort => 'Error connecting to Zulip. Retrying…'; + String get errorConnectingToServerShort => + 'Error connecting to Zulip. Retrying…'; @override String errorConnectingToServerDetails(String serverUrl, String error) { @@ -210,10 +221,15 @@ class ZulipLocalizationsJa extends ZulipLocalizations { } @override - String get errorHandlingEventTitle => 'Error handling a Zulip event. Retrying connection…'; + String get errorHandlingEventTitle => + 'Error handling a Zulip event. Retrying connection…'; @override - String errorHandlingEventDetails(String serverUrl, String error, String event) { + String errorHandlingEventDetails( + String serverUrl, + String error, + String event, + ) { return 'Error handling a Zulip event from $serverUrl; will retry.\n\nError: $error\n\nEvent: $event'; } @@ -256,10 +272,12 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get successMessageLinkCopied => 'Message link copied'; @override - String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; + String get errorBannerDeactivatedDmLabel => + 'You cannot send messages to deactivated users.'; @override - String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + String get errorBannerCannotPostInChannelLabel => + 'You do not have permission to post in this channel.'; @override String get composeBoxAttachFilesTooltip => 'Attach files'; @@ -328,16 +346,19 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get messageListGroupYouWithYourself => 'Messages with yourself'; @override - String get contentValidationErrorTooLong => 'Message length shouldn\'t be greater than 10000 characters.'; + String get contentValidationErrorTooLong => + 'Message length shouldn\'t be greater than 10000 characters.'; @override String get contentValidationErrorEmpty => 'You have nothing to send!'; @override - String get contentValidationErrorQuoteAndReplyInProgress => 'Please wait for the quotation to complete.'; + String get contentValidationErrorQuoteAndReplyInProgress => + 'Please wait for the quotation to complete.'; @override - String get contentValidationErrorUploadInProgress => 'Please wait for the upload to complete.'; + String get contentValidationErrorUploadInProgress => + 'Please wait for the upload to complete.'; @override String get dialogCancel => 'Cancel'; @@ -411,13 +432,19 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get loginErrorMissingUsername => 'Please enter your username.'; @override - String get topicValidationErrorTooLong => 'Topic length shouldn\'t be greater than 60 characters.'; + String get topicValidationErrorTooLong => + 'Topic length shouldn\'t be greater than 60 characters.'; @override - String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.'; + String get topicValidationErrorMandatoryButEmpty => + 'Topics are required in this organization.'; @override - String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) { + String errorServerVersionUnsupportedMessage( + String url, + String zulipVersion, + String minSupportedZulipVersion, + ) { return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.'; } @@ -457,10 +484,12 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get serverUrlValidationErrorInvalidUrl => 'Please enter a valid URL.'; @override - String get serverUrlValidationErrorNoUseEmail => 'Please enter the server URL, not your email.'; + String get serverUrlValidationErrorNoUseEmail => + 'Please enter the server URL, not your email.'; @override - String get serverUrlValidationErrorUnsupportedScheme => 'The server URL must start with http:// or https://.'; + String get serverUrlValidationErrorUnsupportedScheme => + 'The server URL must start with http:// or https://.'; @override String get spoilerDefaultHeaderText => 'Spoiler'; @@ -655,13 +684,15 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get experimentalFeatureSettingsPageTitle => 'Experimental features'; @override - String get experimentalFeatureSettingsWarning => 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; + String get experimentalFeatureSettingsWarning => + 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; @override String get errorNotificationOpenTitle => 'Failed to open notification'; @override - String get errorNotificationOpenAccountMissing => 'The account associated with this notification no longer exists.'; + String get errorNotificationOpenAccountMissing => + 'The account associated with this notification no longer exists.'; @override String get errorReactionAddingFailedTitle => 'Adding reaction failed'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index 3c063e91da..d23bd323fd 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -44,7 +44,8 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get logOutConfirmationDialogTitle => 'Log out?'; @override - String get logOutConfirmationDialogMessage => 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; + String get logOutConfirmationDialogMessage => + 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; @override String get logOutConfirmationDialogConfirmButton => 'Log out'; @@ -65,10 +66,12 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get permissionsNeededOpenSettings => 'Open settings'; @override - String get permissionsDeniedCameraAccess => 'To upload an image, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedCameraAccess => + 'To upload an image, please grant Zulip additional permissions in Settings.'; @override - String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedReadExternalStorage => + 'To upload files, please grant Zulip additional permissions in Settings.'; @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; @@ -95,7 +98,8 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved'; @override - String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved'; + String get errorUnresolveTopicFailedTitle => + 'Failed to mark topic as unresolved'; @override String get actionSheetOptionCopyMessageText => 'Copy message text'; @@ -136,7 +140,8 @@ class ZulipLocalizationsNb extends ZulipLocalizations { } @override - String get errorCouldNotFetchMessageSource => 'Could not fetch message source'; + String get errorCouldNotFetchMessageSource => + 'Could not fetch message source'; @override String get errorCopyingFailed => 'Copying failed'; @@ -152,7 +157,11 @@ class ZulipLocalizationsNb extends ZulipLocalizations { } @override - String errorFilesTooLarge(int num, int maxFileUploadSizeMib, String listMessage) { + String errorFilesTooLarge( + int num, + int maxFileUploadSizeMib, + String listMessage, + ) { String _temp0 = intl.Intl.pluralLogic( num, locale: localeName, @@ -191,7 +200,8 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get errorCouldNotConnectTitle => 'Could not connect'; @override - String get errorMessageDoesNotSeemToExist => 'That message does not seem to exist.'; + String get errorMessageDoesNotSeemToExist => + 'That message does not seem to exist.'; @override String get errorQuotationFailed => 'Quotation failed'; @@ -202,7 +212,8 @@ class ZulipLocalizationsNb extends ZulipLocalizations { } @override - String get errorConnectingToServerShort => 'Error connecting to Zulip. Retrying…'; + String get errorConnectingToServerShort => + 'Error connecting to Zulip. Retrying…'; @override String errorConnectingToServerDetails(String serverUrl, String error) { @@ -210,10 +221,15 @@ class ZulipLocalizationsNb extends ZulipLocalizations { } @override - String get errorHandlingEventTitle => 'Error handling a Zulip event. Retrying connection…'; + String get errorHandlingEventTitle => + 'Error handling a Zulip event. Retrying connection…'; @override - String errorHandlingEventDetails(String serverUrl, String error, String event) { + String errorHandlingEventDetails( + String serverUrl, + String error, + String event, + ) { return 'Error handling a Zulip event from $serverUrl; will retry.\n\nError: $error\n\nEvent: $event'; } @@ -256,10 +272,12 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get successMessageLinkCopied => 'Message link copied'; @override - String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; + String get errorBannerDeactivatedDmLabel => + 'You cannot send messages to deactivated users.'; @override - String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + String get errorBannerCannotPostInChannelLabel => + 'You do not have permission to post in this channel.'; @override String get composeBoxAttachFilesTooltip => 'Attach files'; @@ -328,16 +346,19 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get messageListGroupYouWithYourself => 'Messages with yourself'; @override - String get contentValidationErrorTooLong => 'Message length shouldn\'t be greater than 10000 characters.'; + String get contentValidationErrorTooLong => + 'Message length shouldn\'t be greater than 10000 characters.'; @override String get contentValidationErrorEmpty => 'You have nothing to send!'; @override - String get contentValidationErrorQuoteAndReplyInProgress => 'Please wait for the quotation to complete.'; + String get contentValidationErrorQuoteAndReplyInProgress => + 'Please wait for the quotation to complete.'; @override - String get contentValidationErrorUploadInProgress => 'Please wait for the upload to complete.'; + String get contentValidationErrorUploadInProgress => + 'Please wait for the upload to complete.'; @override String get dialogCancel => 'Cancel'; @@ -411,13 +432,19 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get loginErrorMissingUsername => 'Please enter your username.'; @override - String get topicValidationErrorTooLong => 'Topic length shouldn\'t be greater than 60 characters.'; + String get topicValidationErrorTooLong => + 'Topic length shouldn\'t be greater than 60 characters.'; @override - String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.'; + String get topicValidationErrorMandatoryButEmpty => + 'Topics are required in this organization.'; @override - String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) { + String errorServerVersionUnsupportedMessage( + String url, + String zulipVersion, + String minSupportedZulipVersion, + ) { return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.'; } @@ -457,10 +484,12 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get serverUrlValidationErrorInvalidUrl => 'Please enter a valid URL.'; @override - String get serverUrlValidationErrorNoUseEmail => 'Please enter the server URL, not your email.'; + String get serverUrlValidationErrorNoUseEmail => + 'Please enter the server URL, not your email.'; @override - String get serverUrlValidationErrorUnsupportedScheme => 'The server URL must start with http:// or https://.'; + String get serverUrlValidationErrorUnsupportedScheme => + 'The server URL must start with http:// or https://.'; @override String get spoilerDefaultHeaderText => 'Spoiler'; @@ -655,13 +684,15 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get experimentalFeatureSettingsPageTitle => 'Experimental features'; @override - String get experimentalFeatureSettingsWarning => 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; + String get experimentalFeatureSettingsWarning => + 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; @override String get errorNotificationOpenTitle => 'Failed to open notification'; @override - String get errorNotificationOpenAccountMissing => 'The account associated with this notification no longer exists.'; + String get errorNotificationOpenAccountMissing => + 'The account associated with this notification no longer exists.'; @override String get errorReactionAddingFailedTitle => 'Adding reaction failed'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index d4c3a033d9..e1a6bd45f4 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -44,7 +44,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get logOutConfirmationDialogTitle => 'Wylogować?'; @override - String get logOutConfirmationDialogMessage => 'Aby użyć tego konta należy wypełnić URL organizacji oraz dane konta.'; + String get logOutConfirmationDialogMessage => + 'Aby użyć tego konta należy wypełnić URL organizacji oraz dane konta.'; @override String get logOutConfirmationDialogConfirmButton => 'Wyloguj'; @@ -56,7 +57,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get profileButtonSendDirectMessage => 'Wyślij wiadomość bezpośrednią'; @override - String get errorCouldNotShowUserProfile => 'Nie udało się wyświetlić profilu.'; + String get errorCouldNotShowUserProfile => + 'Nie udało się wyświetlić profilu.'; @override String get permissionsNeededTitle => 'Wymagane uprawnienia'; @@ -65,13 +67,16 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get permissionsNeededOpenSettings => 'Otwórz ustawienia'; @override - String get permissionsDeniedCameraAccess => 'Aby odebrać obraz Zulip musi uzyskać dodatkowe uprawnienia w Ustawieniach.'; + String get permissionsDeniedCameraAccess => + 'Aby odebrać obraz Zulip musi uzyskać dodatkowe uprawnienia w Ustawieniach.'; @override - String get permissionsDeniedReadExternalStorage => 'Aby odebrać pliki Zulip musi uzyskać dodatkowe uprawnienia w Ustawieniach.'; + String get permissionsDeniedReadExternalStorage => + 'Aby odebrać pliki Zulip musi uzyskać dodatkowe uprawnienia w Ustawieniach.'; @override - String get actionSheetOptionMarkChannelAsRead => 'Oznacz kanał jako przeczytany'; + String get actionSheetOptionMarkChannelAsRead => + 'Oznacz kanał jako przeczytany'; @override String get actionSheetOptionMuteTopic => 'Wycisz wątek'; @@ -92,19 +97,23 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get actionSheetOptionUnresolveTopic => 'Oznacz brak rozwiązania'; @override - String get errorResolveTopicFailedTitle => 'Nie udało się oznaczyć jako rozwiązany'; + String get errorResolveTopicFailedTitle => + 'Nie udało się oznaczyć jako rozwiązany'; @override - String get errorUnresolveTopicFailedTitle => 'Nie udało się oznaczyć brak rozwiązania'; + String get errorUnresolveTopicFailedTitle => + 'Nie udało się oznaczyć brak rozwiązania'; @override String get actionSheetOptionCopyMessageText => 'Skopiuj tekst wiadomości'; @override - String get actionSheetOptionCopyMessageLink => 'Skopiuj odnośnik do wiadomości'; + String get actionSheetOptionCopyMessageLink => + 'Skopiuj odnośnik do wiadomości'; @override - String get actionSheetOptionMarkAsUnread => 'Odtąd oznacz jako nieprzeczytane'; + String get actionSheetOptionMarkAsUnread => + 'Odtąd oznacz jako nieprzeczytane'; @override String get actionSheetOptionShare => 'Udostępnij'; @@ -119,7 +128,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get actionSheetOptionUnstarMessage => 'Odbierz gwiazdkę'; @override - String get actionSheetOptionMarkTopicAsRead => 'Oznacz wątek jako przeczytany'; + String get actionSheetOptionMarkTopicAsRead => + 'Oznacz wątek jako przeczytany'; @override String get errorWebAuthOperationalErrorTitle => 'Coś poszło nie tak'; @@ -136,7 +146,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { } @override - String get errorCouldNotFetchMessageSource => 'Nie można uzyskać źródłowej wiadomości'; + String get errorCouldNotFetchMessageSource => + 'Nie można uzyskać źródłowej wiadomości'; @override String get errorCopyingFailed => 'Nie udało się skopiować'; @@ -152,7 +163,11 @@ class ZulipLocalizationsPl extends ZulipLocalizations { } @override - String errorFilesTooLarge(int num, int maxFileUploadSizeMib, String listMessage) { + String errorFilesTooLarge( + int num, + int maxFileUploadSizeMib, + String listMessage, + ) { String _temp0 = intl.Intl.pluralLogic( num, locale: localeName, @@ -191,7 +206,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get errorCouldNotConnectTitle => 'Brak połączenia'; @override - String get errorMessageDoesNotSeemToExist => 'Taka wiadomość raczej nie istnieje.'; + String get errorMessageDoesNotSeemToExist => + 'Taka wiadomość raczej nie istnieje.'; @override String get errorQuotationFailed => 'Cytowanie bez powodzenia'; @@ -202,7 +218,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { } @override - String get errorConnectingToServerShort => 'Błąd połączenia z Zulip. Ponawiam…'; + String get errorConnectingToServerShort => + 'Błąd połączenia z Zulip. Ponawiam…'; @override String errorConnectingToServerDetails(String serverUrl, String error) { @@ -210,10 +227,15 @@ class ZulipLocalizationsPl extends ZulipLocalizations { } @override - String get errorHandlingEventTitle => 'Błąd obsługi zdarzenia Zulip. Ponnawiam połączenie…'; + String get errorHandlingEventTitle => + 'Błąd obsługi zdarzenia Zulip. Ponnawiam połączenie…'; @override - String errorHandlingEventDetails(String serverUrl, String error, String event) { + String errorHandlingEventDetails( + String serverUrl, + String error, + String event, + ) { return 'Błąd zdarzenia Zulip z $serverUrl; ponawiam.\n\nBłąd: $error\n\nZdarzenie: $event'; } @@ -244,7 +266,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get errorStarMessageFailedTitle => 'Dodanie gwiazdki bez powodzenia'; @override - String get errorUnstarMessageFailedTitle => 'Odebranie gwiazdki bez powodzenia'; + String get errorUnstarMessageFailedTitle => + 'Odebranie gwiazdki bez powodzenia'; @override String get successLinkCopied => 'Skopiowano odnośnik'; @@ -256,10 +279,12 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get successMessageLinkCopied => 'Skopiowano odnośnik wiadomości'; @override - String get errorBannerDeactivatedDmLabel => 'Nie można wysyłać wiadomości do dezaktywowanych użytkowników.'; + String get errorBannerDeactivatedDmLabel => + 'Nie można wysyłać wiadomości do dezaktywowanych użytkowników.'; @override - String get errorBannerCannotPostInChannelLabel => 'Nie masz uprawnień do dodawania wpisów w tym kanale.'; + String get errorBannerCannotPostInChannelLabel => + 'Nie masz uprawnień do dodawania wpisów w tym kanale.'; @override String get composeBoxAttachFilesTooltip => 'Dołącz pliki'; @@ -328,16 +353,19 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get messageListGroupYouWithYourself => 'Zapiski na własne konto'; @override - String get contentValidationErrorTooLong => 'Wiadomość nie może być dłuższa niż 10000 znaków.'; + String get contentValidationErrorTooLong => + 'Wiadomość nie może być dłuższa niż 10000 znaków.'; @override String get contentValidationErrorEmpty => 'Nie masz nic do wysłania!'; @override - String get contentValidationErrorQuoteAndReplyInProgress => 'Zaczekaj na zakończenie pobierania cytatu.'; + String get contentValidationErrorQuoteAndReplyInProgress => + 'Zaczekaj na zakończenie pobierania cytatu.'; @override - String get contentValidationErrorUploadInProgress => 'Zaczekaj na zakończenie przekazywania.'; + String get contentValidationErrorUploadInProgress => + 'Zaczekaj na zakończenie przekazywania.'; @override String get dialogCancel => 'Anuluj'; @@ -411,13 +439,19 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get loginErrorMissingUsername => 'Proszę podaj nazwę użytkownika.'; @override - String get topicValidationErrorTooLong => 'Tytuł nie może być dłuższy niż 60 znaków.'; + String get topicValidationErrorTooLong => + 'Tytuł nie może być dłuższy niż 60 znaków.'; @override - String get topicValidationErrorMandatoryButEmpty => 'Wątki są wymagane przez tę organizację.'; + String get topicValidationErrorMandatoryButEmpty => + 'Wątki są wymagane przez tę organizację.'; @override - String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) { + String errorServerVersionUnsupportedMessage( + String url, + String zulipVersion, + String minSupportedZulipVersion, + ) { return '$url uruchamia Zulip Server $zulipVersion, który nie jest obsługiwany. Minimalna obsługiwana wersja to Zulip Server $minSupportedZulipVersion.'; } @@ -457,10 +491,12 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get serverUrlValidationErrorInvalidUrl => 'Proszę podaj poprawny URL.'; @override - String get serverUrlValidationErrorNoUseEmail => 'Proszę podaj adres URL serwera a nie swój email.'; + String get serverUrlValidationErrorNoUseEmail => + 'Proszę podaj adres URL serwera a nie swój email.'; @override - String get serverUrlValidationErrorUnsupportedScheme => 'Adres URL serwera musi zaczynać się od http:// or https://.'; + String get serverUrlValidationErrorUnsupportedScheme => + 'Adres URL serwera musi zaczynać się od http:// or https://.'; @override String get spoilerDefaultHeaderText => 'Spoiler'; @@ -483,7 +519,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get markAsReadInProgress => 'Oznaczanie wiadomości jako przeczytane…'; @override - String get errorMarkAsReadFailedTitle => 'Oznaczanie jako przeczytane bez powodzenia'; + String get errorMarkAsReadFailedTitle => + 'Oznaczanie jako przeczytane bez powodzenia'; @override String markAsUnreadComplete(int num) { @@ -500,7 +537,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get markAsUnreadInProgress => 'Oznaczanie jako nieprzeczytane…'; @override - String get errorMarkAsUnreadFailedTitle => 'Oznaczanie jako nieprzeczytane bez powodzenia'; + String get errorMarkAsUnreadFailedTitle => + 'Oznaczanie jako nieprzeczytane bez powodzenia'; @override String get today => 'Dzisiaj'; @@ -655,19 +693,23 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get experimentalFeatureSettingsPageTitle => 'Funkcje eksperymentalne'; @override - String get experimentalFeatureSettingsWarning => 'W ten sposób aktywujesz funkcje, które są w fazie testów. Mogą one nie działać lub powodować problemy z tym co bez nich działa poprawnie.\n\nTo ustawienie przewidziane jest dla tych, którzy pracują nad ulepszeniem aplikacji Zulip.'; + String get experimentalFeatureSettingsWarning => + 'W ten sposób aktywujesz funkcje, które są w fazie testów. Mogą one nie działać lub powodować problemy z tym co bez nich działa poprawnie.\n\nTo ustawienie przewidziane jest dla tych, którzy pracują nad ulepszeniem aplikacji Zulip.'; @override - String get errorNotificationOpenTitle => 'Otwieranie powiadomienia bez powodzenia'; + String get errorNotificationOpenTitle => + 'Otwieranie powiadomienia bez powodzenia'; @override - String get errorNotificationOpenAccountMissing => 'Konto związane z tym powiadomieniem już nie istnieje.'; + String get errorNotificationOpenAccountMissing => + 'Konto związane z tym powiadomieniem już nie istnieje.'; @override String get errorReactionAddingFailedTitle => 'Dodanie reakcji bez powodzenia'; @override - String get errorReactionRemovingFailedTitle => 'Usuwanie reakcji bez powodzenia'; + String get errorReactionRemovingFailedTitle => + 'Usuwanie reakcji bez powodzenia'; @override String get emojiReactionsMore => 'więcej'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index cb09ca516e..78b68e812a 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -44,7 +44,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get logOutConfirmationDialogTitle => 'Выйти из системы?'; @override - String get logOutConfirmationDialogMessage => 'Чтобы использовать эту учетную запись в будущем, вам придется заново ввести URL-адрес вашей организации и информацию о вашей учетной записи.'; + String get logOutConfirmationDialogMessage => + 'Чтобы использовать эту учетную запись в будущем, вам придется заново ввести URL-адрес вашей организации и информацию о вашей учетной записи.'; @override String get logOutConfirmationDialogConfirmButton => 'Выйти'; @@ -56,7 +57,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get profileButtonSendDirectMessage => 'Отправить личное сообщение'; @override - String get errorCouldNotShowUserProfile => 'Не удалось показать профиль пользователя.'; + String get errorCouldNotShowUserProfile => + 'Не удалось показать профиль пользователя.'; @override String get permissionsNeededTitle => 'Требуются разрешения'; @@ -65,13 +67,16 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get permissionsNeededOpenSettings => 'Открыть настройки'; @override - String get permissionsDeniedCameraAccess => 'Для загрузки изображения, пожалуйста, предоставьте Zulip дополнительные разрешения в настройках.'; + String get permissionsDeniedCameraAccess => + 'Для загрузки изображения, пожалуйста, предоставьте Zulip дополнительные разрешения в настройках.'; @override - String get permissionsDeniedReadExternalStorage => 'Для загрузки файлов, пожалуйста, предоставьте Zulip дополнительные разрешения в настройках.'; + String get permissionsDeniedReadExternalStorage => + 'Для загрузки файлов, пожалуйста, предоставьте Zulip дополнительные разрешения в настройках.'; @override - String get actionSheetOptionMarkChannelAsRead => 'Отметить канал как прочитанный'; + String get actionSheetOptionMarkChannelAsRead => + 'Отметить канал как прочитанный'; @override String get actionSheetOptionMuteTopic => 'Отключить тему'; @@ -92,19 +97,23 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get actionSheetOptionUnresolveTopic => 'Снять отметку \"решено\"'; @override - String get errorResolveTopicFailedTitle => 'Не удалось отметить тему как решенную'; + String get errorResolveTopicFailedTitle => + 'Не удалось отметить тему как решенную'; @override - String get errorUnresolveTopicFailedTitle => 'Не удалось отметить тему как нерешенную'; + String get errorUnresolveTopicFailedTitle => + 'Не удалось отметить тему как нерешенную'; @override String get actionSheetOptionCopyMessageText => 'Скопировать текст сообщения'; @override - String get actionSheetOptionCopyMessageLink => 'Скопировать ссылку на сообщение'; + String get actionSheetOptionCopyMessageLink => + 'Скопировать ссылку на сообщение'; @override - String get actionSheetOptionMarkAsUnread => 'Отметить как непрочитанные начиная отсюда'; + String get actionSheetOptionMarkAsUnread => + 'Отметить как непрочитанные начиная отсюда'; @override String get actionSheetOptionShare => 'Поделиться'; @@ -119,7 +128,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get actionSheetOptionUnstarMessage => 'Снять отметку с сообщения'; @override - String get actionSheetOptionMarkTopicAsRead => 'Отметить тему как прочитанную'; + String get actionSheetOptionMarkTopicAsRead => + 'Отметить тему как прочитанную'; @override String get errorWebAuthOperationalErrorTitle => 'Что-то пошло не так'; @@ -136,7 +146,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { } @override - String get errorCouldNotFetchMessageSource => 'Не удалось извлечь источник сообщения'; + String get errorCouldNotFetchMessageSource => + 'Не удалось извлечь источник сообщения'; @override String get errorCopyingFailed => 'Сбой копирования'; @@ -152,7 +163,11 @@ class ZulipLocalizationsRu extends ZulipLocalizations { } @override - String errorFilesTooLarge(int num, int maxFileUploadSizeMib, String listMessage) { + String errorFilesTooLarge( + int num, + int maxFileUploadSizeMib, + String listMessage, + ) { String _temp0 = intl.Intl.pluralLogic( num, locale: localeName, @@ -191,7 +206,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get errorCouldNotConnectTitle => 'Нет связи с сервером'; @override - String get errorMessageDoesNotSeemToExist => 'Это сообщение, похоже, отсутствует.'; + String get errorMessageDoesNotSeemToExist => + 'Это сообщение, похоже, отсутствует.'; @override String get errorQuotationFailed => 'Цитирование не удалось'; @@ -202,7 +218,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { } @override - String get errorConnectingToServerShort => 'Ошибка подключения к Zulip. Повторяем попытку…'; + String get errorConnectingToServerShort => + 'Ошибка подключения к Zulip. Повторяем попытку…'; @override String errorConnectingToServerDetails(String serverUrl, String error) { @@ -210,10 +227,15 @@ class ZulipLocalizationsRu extends ZulipLocalizations { } @override - String get errorHandlingEventTitle => 'Ошибка обработки события Zulip. Повторная попытка соединения…'; + String get errorHandlingEventTitle => + 'Ошибка обработки события Zulip. Повторная попытка соединения…'; @override - String errorHandlingEventDetails(String serverUrl, String error, String event) { + String errorHandlingEventDetails( + String serverUrl, + String error, + String event, + ) { return 'Ошибка обработки события Zulip от $serverUrl; повторим попытку.\n\nОшибка: $error\n\nСобытие: $event'; } @@ -235,7 +257,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get errorFollowTopicFailed => 'Не удалось начать отслеживать тему'; @override - String get errorUnfollowTopicFailed => 'Не удалось прекратить отслеживать тему'; + String get errorUnfollowTopicFailed => + 'Не удалось прекратить отслеживать тему'; @override String get errorSharingFailed => 'Не удалось поделиться'; @@ -244,7 +267,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get errorStarMessageFailedTitle => 'Не удалось отметить сообщение'; @override - String get errorUnstarMessageFailedTitle => 'Не удалось снять отметку с сообщения'; + String get errorUnstarMessageFailedTitle => + 'Не удалось снять отметку с сообщения'; @override String get successLinkCopied => 'Ссылка скопирована'; @@ -256,10 +280,12 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get successMessageLinkCopied => 'Ссылка на сообщение скопирована'; @override - String get errorBannerDeactivatedDmLabel => 'Нельзя отправить сообщение отключенным пользователям.'; + String get errorBannerDeactivatedDmLabel => + 'Нельзя отправить сообщение отключенным пользователям.'; @override - String get errorBannerCannotPostInChannelLabel => 'У вас нет права писать в этом канале.'; + String get errorBannerCannotPostInChannelLabel => + 'У вас нет права писать в этом канале.'; @override String get composeBoxAttachFilesTooltip => 'Прикрепить файлы'; @@ -328,16 +354,19 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get messageListGroupYouWithYourself => 'Сообщения с собой'; @override - String get contentValidationErrorTooLong => 'Длина сообщения не должна превышать 10000 символов.'; + String get contentValidationErrorTooLong => + 'Длина сообщения не должна превышать 10000 символов.'; @override String get contentValidationErrorEmpty => 'Нечего отправлять!'; @override - String get contentValidationErrorQuoteAndReplyInProgress => 'Пожалуйста, дождитесь завершения цитирования.'; + String get contentValidationErrorQuoteAndReplyInProgress => + 'Пожалуйста, дождитесь завершения цитирования.'; @override - String get contentValidationErrorUploadInProgress => 'Пожалуйста, дождитесь завершения загрузки.'; + String get contentValidationErrorUploadInProgress => + 'Пожалуйста, дождитесь завершения загрузки.'; @override String get dialogCancel => 'Отмена'; @@ -396,7 +425,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get loginEmailLabel => 'Адрес почты'; @override - String get loginErrorMissingEmail => 'Пожалуйста, введите ваш адрес электронной почты.'; + String get loginErrorMissingEmail => + 'Пожалуйста, введите ваш адрес электронной почты.'; @override String get loginPasswordLabel => 'Пароль'; @@ -408,16 +438,23 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get loginUsernameLabel => 'Имя пользователя'; @override - String get loginErrorMissingUsername => 'Пожалуйста, введите ваше имя пользователя.'; + String get loginErrorMissingUsername => + 'Пожалуйста, введите ваше имя пользователя.'; @override - String get topicValidationErrorTooLong => 'Длина темы не должна превышать 60 символов.'; + String get topicValidationErrorTooLong => + 'Длина темы не должна превышать 60 символов.'; @override - String get topicValidationErrorMandatoryButEmpty => 'Темы обязательны в этой организации.'; + String get topicValidationErrorMandatoryButEmpty => + 'Темы обязательны в этой организации.'; @override - String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) { + String errorServerVersionUnsupportedMessage( + String url, + String zulipVersion, + String minSupportedZulipVersion, + ) { return '$url использует Zulip Server $zulipVersion, который не поддерживается. Минимальная поддерживаемая версия — Zulip Server $minSupportedZulipVersion.'; } @@ -454,13 +491,16 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get serverUrlValidationErrorEmpty => 'Пожалуйста, введите URL-адрес.'; @override - String get serverUrlValidationErrorInvalidUrl => 'Пожалуйста, введите корректный URL-адрес.'; + String get serverUrlValidationErrorInvalidUrl => + 'Пожалуйста, введите корректный URL-адрес.'; @override - String get serverUrlValidationErrorNoUseEmail => 'Пожалуйста, введите URL-адрес сервера, а не свой email.'; + String get serverUrlValidationErrorNoUseEmail => + 'Пожалуйста, введите URL-адрес сервера, а не свой email.'; @override - String get serverUrlValidationErrorUnsupportedScheme => 'URL-адрес сервера должен начинаться с http:// или https://.'; + String get serverUrlValidationErrorUnsupportedScheme => + 'URL-адрес сервера должен начинаться с http:// или https://.'; @override String get spoilerDefaultHeaderText => 'Спойлер'; @@ -483,7 +523,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get markAsReadInProgress => 'Помечаем сообщения как прочитанные…'; @override - String get errorMarkAsReadFailedTitle => 'Не удалось установить отметку прочтения'; + String get errorMarkAsReadFailedTitle => + 'Не удалось установить отметку прочтения'; @override String markAsUnreadComplete(int num) { @@ -500,7 +541,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get markAsUnreadInProgress => 'Помечаем сообщения как непрочитанные…'; @override - String get errorMarkAsUnreadFailedTitle => 'Не удалось снять отметку прочтения'; + String get errorMarkAsUnreadFailedTitle => + 'Не удалось снять отметку прочтения'; @override String get today => 'Сегодня'; @@ -652,16 +694,19 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get pollWidgetOptionsMissing => 'В опросе пока нет вариантов ответа.'; @override - String get experimentalFeatureSettingsPageTitle => 'Экспериментальные функции'; + String get experimentalFeatureSettingsPageTitle => + 'Экспериментальные функции'; @override - String get experimentalFeatureSettingsWarning => 'Эти параметры включают функции, которые все еще находятся в стадии разработки и не готовы. Они могут не работать и вызывать проблемы в других местах приложения.\n\nЦель этих настроек — экспериментирование людьми, работающими над разработкой Zulip.'; + String get experimentalFeatureSettingsWarning => + 'Эти параметры включают функции, которые все еще находятся в стадии разработки и не готовы. Они могут не работать и вызывать проблемы в других местах приложения.\n\nЦель этих настроек — экспериментирование людьми, работающими над разработкой Zulip.'; @override String get errorNotificationOpenTitle => 'Не удалось открыть оповещения'; @override - String get errorNotificationOpenAccountMissing => 'Учетной записи, связанной с этим оповещением, больше нет.'; + String get errorNotificationOpenAccountMissing => + 'Учетной записи, связанной с этим оповещением, больше нет.'; @override String get errorReactionAddingFailedTitle => 'Не удалось добавить реакцию'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 0cb42c3a37..193ac26d8e 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -44,7 +44,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get logOutConfirmationDialogTitle => 'Chcete sa odhlásiť?'; @override - String get logOutConfirmationDialogMessage => 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; + String get logOutConfirmationDialogMessage => + 'To use this account in the future, you will have to re-enter the URL for your organization and your account information.'; @override String get logOutConfirmationDialogConfirmButton => 'Odhlásiť sa'; @@ -65,10 +66,12 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get permissionsNeededOpenSettings => 'Otvoriť nastavenia'; @override - String get permissionsDeniedCameraAccess => 'To upload an image, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedCameraAccess => + 'To upload an image, please grant Zulip additional permissions in Settings.'; @override - String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.'; + String get permissionsDeniedReadExternalStorage => + 'To upload files, please grant Zulip additional permissions in Settings.'; @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; @@ -95,7 +98,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get errorResolveTopicFailedTitle => 'Failed to mark topic as resolved'; @override - String get errorUnresolveTopicFailedTitle => 'Failed to mark topic as unresolved'; + String get errorUnresolveTopicFailedTitle => + 'Failed to mark topic as unresolved'; @override String get actionSheetOptionCopyMessageText => 'Skopírovať text správy'; @@ -104,7 +108,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get actionSheetOptionCopyMessageLink => 'Skopírovať odkaz do správy'; @override - String get actionSheetOptionMarkAsUnread => 'Označiť ako neprečítané od tejto správy'; + String get actionSheetOptionMarkAsUnread => + 'Označiť ako neprečítané od tejto správy'; @override String get actionSheetOptionShare => 'Zdielať'; @@ -136,7 +141,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations { } @override - String get errorCouldNotFetchMessageSource => 'Nepodarilo sa nahrať zdroj správy'; + String get errorCouldNotFetchMessageSource => + 'Nepodarilo sa nahrať zdroj správy'; @override String get errorCopyingFailed => 'Kopírovanie zlyhalo'; @@ -152,7 +158,11 @@ class ZulipLocalizationsSk extends ZulipLocalizations { } @override - String errorFilesTooLarge(int num, int maxFileUploadSizeMib, String listMessage) { + String errorFilesTooLarge( + int num, + int maxFileUploadSizeMib, + String listMessage, + ) { String _temp0 = intl.Intl.pluralLogic( num, locale: localeName, @@ -202,7 +212,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations { } @override - String get errorConnectingToServerShort => 'Chyba pri pripájaní na Zulip. Skúšam znovu…'; + String get errorConnectingToServerShort => + 'Chyba pri pripájaní na Zulip. Skúšam znovu…'; @override String errorConnectingToServerDetails(String serverUrl, String error) { @@ -210,10 +221,15 @@ class ZulipLocalizationsSk extends ZulipLocalizations { } @override - String get errorHandlingEventTitle => 'Chyba pri obsluhe Zulip udalosti. Pokúšam sa znovu…'; + String get errorHandlingEventTitle => + 'Chyba pri obsluhe Zulip udalosti. Pokúšam sa znovu…'; @override - String errorHandlingEventDetails(String serverUrl, String error, String event) { + String errorHandlingEventDetails( + String serverUrl, + String error, + String event, + ) { return 'Chyba obsluhy Zulip udalosti na serveri $serverUrl; skúsim znovu.\n\nChyba: $error\n\nUdalosť: $event'; } @@ -256,10 +272,12 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get successMessageLinkCopied => 'Message link copied'; @override - String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; + String get errorBannerDeactivatedDmLabel => + 'You cannot send messages to deactivated users.'; @override - String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + String get errorBannerCannotPostInChannelLabel => + 'You do not have permission to post in this channel.'; @override String get composeBoxAttachFilesTooltip => 'Attach files'; @@ -328,16 +346,19 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get messageListGroupYouWithYourself => 'Messages with yourself'; @override - String get contentValidationErrorTooLong => 'Message length shouldn\'t be greater than 10000 characters.'; + String get contentValidationErrorTooLong => + 'Message length shouldn\'t be greater than 10000 characters.'; @override String get contentValidationErrorEmpty => 'You have nothing to send!'; @override - String get contentValidationErrorQuoteAndReplyInProgress => 'Please wait for the quotation to complete.'; + String get contentValidationErrorQuoteAndReplyInProgress => + 'Please wait for the quotation to complete.'; @override - String get contentValidationErrorUploadInProgress => 'Please wait for the upload to complete.'; + String get contentValidationErrorUploadInProgress => + 'Please wait for the upload to complete.'; @override String get dialogCancel => 'Cancel'; @@ -411,13 +432,19 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get loginErrorMissingUsername => 'Prosím zadajte prihlasovacie meno.'; @override - String get topicValidationErrorTooLong => 'Topic length shouldn\'t be greater than 60 characters.'; + String get topicValidationErrorTooLong => + 'Topic length shouldn\'t be greater than 60 characters.'; @override - String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.'; + String get topicValidationErrorMandatoryButEmpty => + 'Topics are required in this organization.'; @override - String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) { + String errorServerVersionUnsupportedMessage( + String url, + String zulipVersion, + String minSupportedZulipVersion, + ) { return '$url is running Zulip Server $zulipVersion, which is unsupported. The minimum supported version is Zulip Server $minSupportedZulipVersion.'; } @@ -457,10 +484,12 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get serverUrlValidationErrorInvalidUrl => 'Vložte správnu adresu.'; @override - String get serverUrlValidationErrorNoUseEmail => 'Vložte adresu servera, nie email.'; + String get serverUrlValidationErrorNoUseEmail => + 'Vložte adresu servera, nie email.'; @override - String get serverUrlValidationErrorUnsupportedScheme => 'Adresa servera musí začínať s http:// or https://.'; + String get serverUrlValidationErrorUnsupportedScheme => + 'Adresa servera musí začínať s http:// or https://.'; @override String get spoilerDefaultHeaderText => 'Vyzradenie'; @@ -483,7 +512,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get markAsReadInProgress => 'Označiť správy ako prečítané…'; @override - String get errorMarkAsReadFailedTitle => 'Neodarilo sa označiť správy ako prečítané'; + String get errorMarkAsReadFailedTitle => + 'Neodarilo sa označiť správy ako prečítané'; @override String markAsUnreadComplete(int num) { @@ -500,7 +530,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get markAsUnreadInProgress => 'Označiť správy ako neprečítané…'; @override - String get errorMarkAsUnreadFailedTitle => 'Zlyhalo označenie správ za prečítané'; + String get errorMarkAsUnreadFailedTitle => + 'Zlyhalo označenie správ za prečítané'; @override String get today => 'Dnes'; @@ -655,13 +686,15 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get experimentalFeatureSettingsPageTitle => 'Experimental features'; @override - String get experimentalFeatureSettingsWarning => 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; + String get experimentalFeatureSettingsWarning => + 'These options enable features which are still under development and not ready. They may not work, and may cause issues in other areas of the app.\n\nThe purpose of these settings is for experimentation by people working on developing Zulip.'; @override String get errorNotificationOpenTitle => 'Nepodarilo sa otvoriť oznámenie'; @override - String get errorNotificationOpenAccountMissing => 'The account associated with this notification no longer exists.'; + String get errorNotificationOpenAccountMissing => + 'The account associated with this notification no longer exists.'; @override String get errorReactionAddingFailedTitle => 'Nepodarilo sa pridať reakciu'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index b7e6d792f2..c1898631fd 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -44,7 +44,8 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get logOutConfirmationDialogTitle => 'Вийти?'; @override - String get logOutConfirmationDialogMessage => 'Щоб використовувати цей обліковий запис у майбутньому, вам доведеться повторно ввести його дані та URL-адресу вашої організації.'; + String get logOutConfirmationDialogMessage => + 'Щоб використовувати цей обліковий запис у майбутньому, вам доведеться повторно ввести його дані та URL-адресу вашої організації.'; @override String get logOutConfirmationDialogConfirmButton => 'Вийти'; @@ -53,10 +54,12 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get chooseAccountButtonAddAnAccount => 'Додати обліковий запис'; @override - String get profileButtonSendDirectMessage => 'Надіслати особисте повідомлення'; + String get profileButtonSendDirectMessage => + 'Надіслати особисте повідомлення'; @override - String get errorCouldNotShowUserProfile => 'Не вдалося показати профіль користувача.'; + String get errorCouldNotShowUserProfile => + 'Не вдалося показати профіль користувача.'; @override String get permissionsNeededTitle => 'Потрібні дозволи'; @@ -65,13 +68,16 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get permissionsNeededOpenSettings => 'Відкрити налаштування'; @override - String get permissionsDeniedCameraAccess => 'Щоб завантажити зображення, надайте Zulip додаткові дозволи в налаштуваннях.'; + String get permissionsDeniedCameraAccess => + 'Щоб завантажити зображення, надайте Zulip додаткові дозволи в налаштуваннях.'; @override - String get permissionsDeniedReadExternalStorage => 'Щоб завантажувати файли, надайте Zulip додаткові дозволи в налаштуваннях.'; + String get permissionsDeniedReadExternalStorage => + 'Щоб завантажувати файли, надайте Zulip додаткові дозволи в налаштуваннях.'; @override - String get actionSheetOptionMarkChannelAsRead => 'Позначити канал як прочитаний'; + String get actionSheetOptionMarkChannelAsRead => + 'Позначити канал як прочитаний'; @override String get actionSheetOptionMuteTopic => 'Заглушити тему'; @@ -92,16 +98,19 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get actionSheetOptionUnresolveTopic => 'Позначити як невирішене'; @override - String get errorResolveTopicFailedTitle => 'Не вдалося позначити тему як вирішену'; + String get errorResolveTopicFailedTitle => + 'Не вдалося позначити тему як вирішену'; @override - String get errorUnresolveTopicFailedTitle => 'Не вдалося позначити тему як невирішену'; + String get errorUnresolveTopicFailedTitle => + 'Не вдалося позначити тему як невирішену'; @override String get actionSheetOptionCopyMessageText => 'Копіювати текст повідомлення'; @override - String get actionSheetOptionCopyMessageLink => 'Копіювати посилання на повідомлення'; + String get actionSheetOptionCopyMessageLink => + 'Копіювати посилання на повідомлення'; @override String get actionSheetOptionMarkAsUnread => 'Позначити як непрочитане звідси'; @@ -116,7 +125,8 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get actionSheetOptionStarMessage => 'Позначити повідомлення зірочкою'; @override - String get actionSheetOptionUnstarMessage => 'Зняти позначку зірочки з повідомлення'; + String get actionSheetOptionUnstarMessage => + 'Зняти позначку зірочки з повідомлення'; @override String get actionSheetOptionMarkTopicAsRead => 'Позначити тему як прочитану'; @@ -136,7 +146,8 @@ class ZulipLocalizationsUk extends ZulipLocalizations { } @override - String get errorCouldNotFetchMessageSource => 'Не вдалося отримати джерело повідомлення'; + String get errorCouldNotFetchMessageSource => + 'Не вдалося отримати джерело повідомлення'; @override String get errorCopyingFailed => 'Помилка копіювання'; @@ -152,7 +163,11 @@ class ZulipLocalizationsUk extends ZulipLocalizations { } @override - String errorFilesTooLarge(int num, int maxFileUploadSizeMib, String listMessage) { + String errorFilesTooLarge( + int num, + int maxFileUploadSizeMib, + String listMessage, + ) { String _temp0 = intl.Intl.pluralLogic( num, locale: localeName, @@ -191,7 +206,8 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get errorCouldNotConnectTitle => 'Не вдалося підключитися'; @override - String get errorMessageDoesNotSeemToExist => 'Здається, цього повідомлення не існує.'; + String get errorMessageDoesNotSeemToExist => + 'Здається, цього повідомлення не існує.'; @override String get errorQuotationFailed => 'Помилка цитування'; @@ -202,7 +218,8 @@ class ZulipLocalizationsUk extends ZulipLocalizations { } @override - String get errorConnectingToServerShort => 'Помилка підключення до Zulip. Повторна спроба…'; + String get errorConnectingToServerShort => + 'Помилка підключення до Zulip. Повторна спроба…'; @override String errorConnectingToServerDetails(String serverUrl, String error) { @@ -210,10 +227,15 @@ class ZulipLocalizationsUk extends ZulipLocalizations { } @override - String get errorHandlingEventTitle => 'Помилка обробки події Zulip. Повторна спроба підключення…'; + String get errorHandlingEventTitle => + 'Помилка обробки події Zulip. Повторна спроба підключення…'; @override - String errorHandlingEventDetails(String serverUrl, String error, String event) { + String errorHandlingEventDetails( + String serverUrl, + String error, + String event, + ) { return 'Помилка обробки події Zulip із $serverUrl; буде повторювати спробу.\n\nПомилка: $error\n\nПодія: $event'; } @@ -241,10 +263,12 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get errorSharingFailed => 'Поширення не вдалося'; @override - String get errorStarMessageFailedTitle => 'Не вдалося позначити повідомлення зірочкою'; + String get errorStarMessageFailedTitle => + 'Не вдалося позначити повідомлення зірочкою'; @override - String get errorUnstarMessageFailedTitle => 'Не вдалося зняти позначку зірочки з повідомлення'; + String get errorUnstarMessageFailedTitle => + 'Не вдалося зняти позначку зірочки з повідомлення'; @override String get successLinkCopied => 'Посилання скопійовано'; @@ -253,13 +277,16 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get successMessageTextCopied => 'Текст повідомлення скопійовано'; @override - String get successMessageLinkCopied => 'Посилання на повідомлення скопійовано'; + String get successMessageLinkCopied => + 'Посилання на повідомлення скопійовано'; @override - String get errorBannerDeactivatedDmLabel => 'Ви не можете надсилати повідомлення деактивованим користувачам.'; + String get errorBannerDeactivatedDmLabel => + 'Ви не можете надсилати повідомлення деактивованим користувачам.'; @override - String get errorBannerCannotPostInChannelLabel => 'Ви не маєте дозволу на публікацію в цьому каналі.'; + String get errorBannerCannotPostInChannelLabel => + 'Ви не маєте дозволу на публікацію в цьому каналі.'; @override String get composeBoxAttachFilesTooltip => 'Прикріпити файли'; @@ -328,16 +355,19 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get messageListGroupYouWithYourself => 'Повідомлення з собою'; @override - String get contentValidationErrorTooLong => 'Довжина повідомлення не повинна перевищувати 10000 символів.'; + String get contentValidationErrorTooLong => + 'Довжина повідомлення не повинна перевищувати 10000 символів.'; @override String get contentValidationErrorEmpty => 'Вам нема чого надсилати!'; @override - String get contentValidationErrorQuoteAndReplyInProgress => 'Будь ласка, дочекайтеся завершення цитування.'; + String get contentValidationErrorQuoteAndReplyInProgress => + 'Будь ласка, дочекайтеся завершення цитування.'; @override - String get contentValidationErrorUploadInProgress => 'Дочекайтеся завершення завантаження.'; + String get contentValidationErrorUploadInProgress => + 'Дочекайтеся завершення завантаження.'; @override String get dialogCancel => 'Відміна'; @@ -396,7 +426,8 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get loginEmailLabel => 'Адреса електронної пошти'; @override - String get loginErrorMissingEmail => 'Будь ласка, введіть свою електронну адресу.'; + String get loginErrorMissingEmail => + 'Будь ласка, введіть свою електронну адресу.'; @override String get loginPasswordLabel => 'Пароль'; @@ -411,13 +442,19 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get loginErrorMissingUsername => 'Введіть своє ім\'я користувача.'; @override - String get topicValidationErrorTooLong => 'Довжина теми не повинна перевищувати 60 символів.'; + String get topicValidationErrorTooLong => + 'Довжина теми не повинна перевищувати 60 символів.'; @override - String get topicValidationErrorMandatoryButEmpty => 'Теми обовʼязкові в цій організації.'; + String get topicValidationErrorMandatoryButEmpty => + 'Теми обовʼязкові в цій організації.'; @override - String errorServerVersionUnsupportedMessage(String url, String zulipVersion, String minSupportedZulipVersion) { + String errorServerVersionUnsupportedMessage( + String url, + String zulipVersion, + String minSupportedZulipVersion, + ) { return '$url використовує Zulip Server $zulipVersion, який не підтримується. Мінімальною підтримуваною версією є Zulip Server $minSupportedZulipVersion.'; } @@ -457,10 +494,12 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get serverUrlValidationErrorInvalidUrl => 'Введіть дійсну URL-адресу.'; @override - String get serverUrlValidationErrorNoUseEmail => 'Введіть URL-адресу сервера, а не свою електронну адресу.'; + String get serverUrlValidationErrorNoUseEmail => + 'Введіть URL-адресу сервера, а не свою електронну адресу.'; @override - String get serverUrlValidationErrorUnsupportedScheme => 'URL-адреса сервера має починатися з http:// або https://.'; + String get serverUrlValidationErrorUnsupportedScheme => + 'URL-адреса сервера має починатися з http:// або https://.'; @override String get spoilerDefaultHeaderText => 'Спойлер'; @@ -497,10 +536,12 @@ class ZulipLocalizationsUk extends ZulipLocalizations { } @override - String get markAsUnreadInProgress => 'Позначення повідомлень як непрочитаних…'; + String get markAsUnreadInProgress => + 'Позначення повідомлень як непрочитаних…'; @override - String get errorMarkAsUnreadFailedTitle => 'Не вдалося позначити як непрочитане'; + String get errorMarkAsUnreadFailedTitle => + 'Не вдалося позначити як непрочитане'; @override String get today => 'Сьогодні'; @@ -643,25 +684,29 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get themeSettingSystem => 'Системна'; @override - String get openLinksWithInAppBrowser => 'Відкривати посилання за допомогою браузера додатку'; + String get openLinksWithInAppBrowser => + 'Відкривати посилання за допомогою браузера додатку'; @override String get pollWidgetQuestionMissing => 'Немає питання.'; @override - String get pollWidgetOptionsMissing => 'У цьому опитуванні ще немає варіантів.'; + String get pollWidgetOptionsMissing => + 'У цьому опитуванні ще немає варіантів.'; @override String get experimentalFeatureSettingsPageTitle => 'Експериментальні функції'; @override - String get experimentalFeatureSettingsWarning => 'Ці опції вмикають функції, які ще розробляються та не готові. Вони можуть не працювати та викликати проблеми в інших місцях додатку.\n\nМетою цих налаштувань є експериментування людьми, що працюють над розробкою Zulip.'; + String get experimentalFeatureSettingsWarning => + 'Ці опції вмикають функції, які ще розробляються та не готові. Вони можуть не працювати та викликати проблеми в інших місцях додатку.\n\nМетою цих налаштувань є експериментування людьми, що працюють над розробкою Zulip.'; @override String get errorNotificationOpenTitle => 'Не вдалося відкрити сповіщення'; @override - String get errorNotificationOpenAccountMissing => 'Обліковий запис, пов’язаний із цим сповіщенням, більше не існує.'; + String get errorNotificationOpenAccountMissing => + 'Обліковий запис, пов’язаний із цим сповіщенням, більше не існує.'; @override String get errorReactionAddingFailedTitle => 'Не вдалося додати реакцію'; diff --git a/pubspec.lock b/pubspec.lock index f9401bbaf3..a1a7177bc2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1355,5 +1355,5 @@ packages: source: path version: "0.0.1" sdks: - dart: ">=3.9.0-55.0.dev <4.0.0" - flutter: ">=3.32.0-1.0.pre.257" + dart: ">=3.9.0-63.0.dev <4.0.0" + flutter: ">=3.32.0-1.0.pre.332" diff --git a/pubspec.yaml b/pubspec.yaml index 95d05d41c8..a1c9f6dd2d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,8 +14,8 @@ environment: # We use a recent version of Flutter from its main channel, and # the corresponding recent version of the Dart SDK. # Feel free to update these regularly; see README.md for instructions. - sdk: '>=3.9.0-55.0.dev <4.0.0' - flutter: '>=3.32.0-1.0.pre.257' # b90818ec53ac6f774cdeebd2acd9ab8e71b5c7b5 + sdk: '>=3.9.0-63.0.dev <4.0.0' + flutter: '>=3.32.0-1.0.pre.332' # adae8bbdbaed53ef305726fcfe811b2351d73a1a # To update dependencies, see instructions in README.md. dependencies: From 8992f35cdfb6620fed4f2274c092edddfa6455e1 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 4 Apr 2025 19:48:02 -0700 Subject: [PATCH 02/22] msglist [nfc]: Simplify name of _scrollToBottomVisible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This isn't a "value"; it's a "value notifier", which has a value. We could call it "… notifier" instead; but the type seems adequate already for disambiguating that. --- lib/widgets/message_list.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 03ae763bdc..e97f36c56a 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -466,7 +466,7 @@ class MessageList extends StatefulWidget { class _MessageListState extends State with PerAccountStoreAwareStateMixin { MessageListView? model; final ScrollController scrollController = MessageListScrollController(); - final ValueNotifier _scrollToBottomVisibleValue = ValueNotifier(false); + final ValueNotifier _scrollToBottomVisible = ValueNotifier(false); @override void initState() { @@ -484,7 +484,7 @@ class _MessageListState extends State with PerAccountStoreAwareStat void dispose() { model?.dispose(); scrollController.dispose(); - _scrollToBottomVisibleValue.dispose(); + _scrollToBottomVisible.dispose(); super.dispose(); } @@ -511,9 +511,9 @@ class _MessageListState extends State with PerAccountStoreAwareStat void _handleScrollMetrics(ScrollMetrics scrollMetrics) { if (scrollMetrics.extentAfter == 0) { - _scrollToBottomVisibleValue.value = false; + _scrollToBottomVisible.value = false; } else { - _scrollToBottomVisibleValue.value = true; + _scrollToBottomVisible.value = true; } if (scrollMetrics.extentBefore < kFetchMessagesBufferPixels) { @@ -577,7 +577,7 @@ class _MessageListState extends State with PerAccountStoreAwareStat child: SafeArea( child: ScrollToBottomButton( scrollController: scrollController, - visibleValue: _scrollToBottomVisibleValue))), + visible: _scrollToBottomVisible))), ]))))); } @@ -690,9 +690,9 @@ class _MessageListState extends State with PerAccountStoreAwareStat } class ScrollToBottomButton extends StatelessWidget { - const ScrollToBottomButton({super.key, required this.scrollController, required this.visibleValue}); + const ScrollToBottomButton({super.key, required this.scrollController, required this.visible}); - final ValueNotifier visibleValue; + final ValueNotifier visible; final ScrollController scrollController; Future _navigateToBottom() { @@ -709,7 +709,7 @@ class ScrollToBottomButton extends StatelessWidget { Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); return ValueListenableBuilder( - valueListenable: visibleValue, + valueListenable: visible, builder: (BuildContext context, bool value, Widget? child) { return (value && child != null) ? child : const SizedBox.shrink(); }, From 323fc7bd71e1967feb626ba24c53cd9b1eb7baa8 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 5 Apr 2025 22:33:41 -0700 Subject: [PATCH 03/22] msglist [nfc]: Fix name of scroll-to-bottom callback There's no "navigation" happening here -- the user remains on the same page of the app. --- lib/widgets/message_list.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index e97f36c56a..97cf2193b5 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -695,7 +695,7 @@ class ScrollToBottomButton extends StatelessWidget { final ValueNotifier visible; final ScrollController scrollController; - Future _navigateToBottom() { + Future _scrollToBottom() { final distance = scrollController.position.pixels; final durationMsAtSpeedLimit = (1000 * distance / 8000).ceil(); final durationMs = max(300, durationMsAtSpeedLimit); @@ -720,7 +720,7 @@ class ScrollToBottomButton extends StatelessWidget { iconSize: 40, // Web has the same color in light and dark mode. color: const HSLColor.fromAHSL(0.5, 240, 0.96, 0.68).toColor(), - onPressed: _navigateToBottom)); + onPressed: _scrollToBottom)); } } From 6479d6cf53bba9aed2940f0e9ebd3750acc33bda Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 18 Apr 2025 15:21:40 -0700 Subject: [PATCH 04/22] msglist test [nfc]: Clean up tests of scroll-to-bottom button --- test/widgets/message_list_test.dart | 30 ++++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 0262df378e..d7d2143a91 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -462,10 +462,7 @@ void main() { testWidgets('scrolling changes visibility', (tester) async { await setupMessageListPage(tester, messageCount: 10); - final scrollController = findMessageListScrollController(tester)!; - - // Initial state should be not visible, as the message list renders with latest message in view check(isButtonVisible(tester)).equals(false); scrollController.jumpTo(-600); @@ -480,41 +477,38 @@ void main() { testWidgets('dimension updates changes visibility', (tester) async { await setupMessageListPage(tester, messageCount: 100); + // Scroll up, to hide the button. final scrollController = findMessageListScrollController(tester)!; - - // Initial state should be not visible, as the message list renders with latest message in view - check(isButtonVisible(tester)).equals(false); - scrollController.jumpTo(-600); await tester.pump(); check(isButtonVisible(tester)).equals(true); + // Make the view taller, so that the bottom of the list is back in view. addTearDown(tester.view.resetPhysicalSize); tester.view.physicalSize = const Size(2000, 40000); await tester.pump(); - // Dimension changes use NotificationListener + // which has a one-frame lag. If that ever gets resolved, + // this extra pump would ideally be removed.) await tester.pump(); + // Check the button duly disappears again. check(isButtonVisible(tester)).equals(false); }); - testWidgets('button functionality', (tester) async { + testWidgets('button works', (tester) async { await setupMessageListPage(tester, messageCount: 10); - final scrollController = findMessageListScrollController(tester)!; - - // Initial state should be not visible, as the message list renders with latest message in view - check(isButtonVisible(tester)).equals(false); - scrollController.jumpTo(-600); await tester.pump(); - check(isButtonVisible(tester)).equals(true); + check(scrollController.position.pixels).equals(-600); + // Tap button. await tester.tap(find.byType(ScrollToBottomButton)); + // The list scrolls to the end… await tester.pumpAndSettle(); - check(isButtonVisible(tester)).equals(false); check(scrollController.position.pixels).equals(0); + // … and for good measure confirm the button disappeared. + check(isButtonVisible(tester)).equals(false); }); }); From 0605fce63a4ef25fb900d5f067aba0efabf4a349 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 5 Apr 2025 21:31:40 -0700 Subject: [PATCH 05/22] msglist: Fix speed calculations in scroll to bottom Fixes #1485. This logic for deciding how long the scroll-to-bottom animation should take, introduced in 5abeb887e (#223), didn't have any tests. As a result, when its unstated assumption about the message list -- that scrolling up from the end was represented by positive scroll positions -- was broken a few months later (in bdac26fb1), nothing alerted us to that. I did notice a few times over the past year or so that the effect of the scroll-to-bottom button seemed jerky, as if it were trying to move much farther in each frame than it should. Now I know why. (Discovered this in the course of revisiting this code in order to adapt it to the more radical change to the message list's scroll positions which is coming up: "zero" won't be the end, but somewhere in the middle.) --- lib/widgets/message_list.dart | 5 +++-- test/widgets/message_list_test.dart | 34 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 97cf2193b5..e2c7dc5141 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -696,11 +696,12 @@ class ScrollToBottomButton extends StatelessWidget { final ScrollController scrollController; Future _scrollToBottom() { - final distance = scrollController.position.pixels; + final target = 0.0; + final distance = target - scrollController.position.pixels; final durationMsAtSpeedLimit = (1000 * distance / 8000).ceil(); final durationMs = max(300, durationMsAtSpeedLimit); return scrollController.animateTo( - 0, + target, duration: Duration(milliseconds: durationMs), curve: Curves.ease); } diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index d7d2143a91..a904a5ad8d 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -510,6 +510,40 @@ void main() { // … and for good measure confirm the button disappeared. check(isButtonVisible(tester)).equals(false); }); + + testWidgets('scrolls at reasonable speed', (tester) async { + const referenceSpeed = 8000.0; + const distance = 40000.0; + await setupMessageListPage(tester, messageCount: 1000); + final controller = findMessageListScrollController(tester)!; + + // Scroll a long distance up, many screenfuls. + controller.jumpTo(-distance); + await tester.pump(); + check(controller.position.pixels).equals(-distance); + + // Tap button. + await tester.tap(find.byType(ScrollToBottomButton)); + await tester.pump(); + + // Measure speed. + final log = []; + double pos = controller.position.pixels; + while (pos < 0) { + check(log.length).isLessThan(30); + await tester.pump(const Duration(seconds: 1)); + final lastPos = pos; + pos = controller.position.pixels; + log.add(pos - lastPos); + } + // Check the main question: the speed stayed in range throughout. + const maxSpeed = 2 * referenceSpeed; + check(log).every((it) => it..isGreaterThan(0)..isLessThan(maxSpeed)); + // Also check the test's assumptions: the scroll reached the end… + check(pos).equals(0); + // … and scrolled far enough to effectively test the max speed. + check(log.sum).isGreaterThan(2 * maxSpeed); + }); }); group('TypingStatusWidget', () { From 172b2c2ce1e7bc36f60c8dfe3a469f3cf38b8b40 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 18 Apr 2025 16:42:24 -0700 Subject: [PATCH 06/22] msglist: Drop easing curve in scroll-to-bottom Fundamentally an easing curve like this relies on knowing in advance how far the animation is going to end up going. When we start letting the message list scroll from the middle of history, it'll no longer be possible to know that. Switch instead to a behavior that can be based on only what's happened so far, not on a prediction of the future. In the future, if we want, we could get fancy and make the speed change over time; but to start, keep it simple, and just move at the same speed from start to finish. --- lib/widgets/message_list.dart | 2 +- test/widgets/message_list_test.dart | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index e2c7dc5141..132e29a479 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -703,7 +703,7 @@ class ScrollToBottomButton extends StatelessWidget { return scrollController.animateTo( target, duration: Duration(milliseconds: durationMs), - curve: Curves.ease); + curve: Curves.linear); } @override diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index a904a5ad8d..44f9a3203d 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -511,8 +511,8 @@ void main() { check(isButtonVisible(tester)).equals(false); }); - testWidgets('scrolls at reasonable speed', (tester) async { - const referenceSpeed = 8000.0; + testWidgets('scrolls at reasonable, constant speed', (tester) async { + const maxSpeed = 8000.0; const distance = 40000.0; await setupMessageListPage(tester, messageCount: 1000); final controller = findMessageListScrollController(tester)!; @@ -536,9 +536,10 @@ void main() { pos = controller.position.pixels; log.add(pos - lastPos); } - // Check the main question: the speed stayed in range throughout. - const maxSpeed = 2 * referenceSpeed; - check(log).every((it) => it..isGreaterThan(0)..isLessThan(maxSpeed)); + // Check the main question: the speed was as expected throughout. + check(log.slice(0, log.length-1)).every((it) => it.equals(maxSpeed)); + check(log).last..isGreaterThan(0)..isLessOrEqual(maxSpeed); + // Also check the test's assumptions: the scroll reached the end… check(pos).equals(0); // … and scrolled far enough to effectively test the max speed. From 427446736a5f8e5decc2a1023f4747025278d99d Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 4 Apr 2025 19:59:36 -0700 Subject: [PATCH 07/22] msglist [nfc]: Discard scroll-to-bottom animation future up front Nothing awaits this future anyway; this method is only called as a gesture callback. The callback is expected to return void, emphasizing that nothing will inspect its return value. --- lib/widgets/message_list.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 132e29a479..321ae8bdb2 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -695,12 +695,12 @@ class ScrollToBottomButton extends StatelessWidget { final ValueNotifier visible; final ScrollController scrollController; - Future _scrollToBottom() { + void _scrollToBottom() { final target = 0.0; final distance = target - scrollController.position.pixels; final durationMsAtSpeedLimit = (1000 * distance / 8000).ceil(); final durationMs = max(300, durationMsAtSpeedLimit); - return scrollController.animateTo( + scrollController.animateTo( target, duration: Duration(milliseconds: durationMs), curve: Curves.linear); From 8f202795e3bd1a281e8b20e5140a5fde10225678 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 4 Apr 2025 20:35:54 -0700 Subject: [PATCH 08/22] msglist [nfc]: Specify we have a MessageListScrollController --- lib/widgets/message_list.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 321ae8bdb2..7934c589f9 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -465,7 +465,7 @@ class MessageList extends StatefulWidget { class _MessageListState extends State with PerAccountStoreAwareStateMixin { MessageListView? model; - final ScrollController scrollController = MessageListScrollController(); + final MessageListScrollController scrollController = MessageListScrollController(); final ValueNotifier _scrollToBottomVisible = ValueNotifier(false); @override @@ -693,7 +693,7 @@ class ScrollToBottomButton extends StatelessWidget { const ScrollToBottomButton({super.key, required this.scrollController, required this.visible}); final ValueNotifier visible; - final ScrollController scrollController; + final MessageListScrollController scrollController; void _scrollToBottom() { final target = 0.0; From 43e2514530f447ec478c45ec68c6c5b5635d71b0 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 4 Apr 2025 20:37:44 -0700 Subject: [PATCH 09/22] scroll [nfc]: Expose MessageListScrollPosition on MessageListScrollController --- lib/widgets/scrolling.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index 69f46082b3..d2e453d80c 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -180,7 +180,10 @@ class MessageListScrollController extends ScrollController { }); @override - ScrollPosition createScrollPosition(ScrollPhysics physics, + MessageListScrollPosition get position => super.position as MessageListScrollPosition; + + @override + MessageListScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) { return MessageListScrollPosition( physics: physics, From 56adcb466dc82a4d8ce8ebe03b6f6cb8c7679327 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 5 Apr 2025 21:37:49 -0700 Subject: [PATCH 10/22] scroll [nfc]: Move scroll-to-bottom logic into our ScrollPosition subclass This makes a more comfortable home for extending this logic, because it naturally uses a lot of ScrollPosition members. To make the logic fit in better on this class, also loosen its assumptions slightly, allowing maxScrollExtent to be nonzero. It's still expected not to change, though -- fixing that is a more complex job, and will come over the remainder of this commit series. --- lib/widgets/message_list.dart | 11 +---------- lib/widgets/scrolling.dart | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 7934c589f9..9d8e27757a 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_color_models/flutter_color_models.dart'; @@ -696,14 +694,7 @@ class ScrollToBottomButton extends StatelessWidget { final MessageListScrollController scrollController; void _scrollToBottom() { - final target = 0.0; - final distance = target - scrollController.position.pixels; - final durationMsAtSpeedLimit = (1000 * distance / 8000).ceil(); - final durationMs = max(300, durationMsAtSpeedLimit); - scrollController.animateTo( - target, - duration: Duration(milliseconds: durationMs), - curve: Curves.linear); + scrollController.position.scrollToEnd(); } @override diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index d2e453d80c..ed4a674f25 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -167,6 +167,26 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext { return !changed; } + + /// Scroll the position smoothly to the end of the scrollable content. + /// + /// This method only works well if [maxScrollExtent] is accurate + /// and does not change during the animation. + /// (For example, this works if there is no content in forward slivers, + /// so that [maxScrollExtent] is always zero.) + /// The animation will attempt to travel to the value [maxScrollExtent] had + /// at the start of the animation, even if that ends up being more or less far + /// than the actual extent of the content. + void scrollToEnd() { + final target = maxScrollExtent; + final distance = target - pixels; + final durationMsAtSpeedLimit = (1000 * distance / 8000).ceil(); + final durationMs = math.max(300, durationMsAtSpeedLimit); + animateTo( + target, + duration: Duration(milliseconds: durationMs), + curve: Curves.linear); + } } /// A version of [ScrollController] adapted for the Zulip message list. From 1694112d6c0c0e299c47ee04d79686790cab0958 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 18 Apr 2025 17:55:00 -0700 Subject: [PATCH 11/22] scroll test [nfc]: Share a controller variable in MessageListScrollView tests This will be convenient for testing scrollToEnd. --- test/widgets/scrolling_test.dart | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/widgets/scrolling_test.dart b/test/widgets/scrolling_test.dart index 8f1820e6ff..d86259191b 100644 --- a/test/widgets/scrolling_test.dart +++ b/test/widgets/scrolling_test.dart @@ -8,12 +8,12 @@ import '../flutter_checks.dart'; void main() { group('MessageListScrollView', () { Widget buildList({ - MessageListScrollController? controller, + required MessageListScrollController controller, required double topHeight, required double bottomHeight, }) { return MessageListScrollView( - controller: controller ?? MessageListScrollController(), + controller: controller, center: const ValueKey('center'), slivers: [ SliverToBoxAdapter( @@ -23,11 +23,16 @@ void main() { ]); } + late MessageListScrollController controller; + Future prepare(WidgetTester tester, { - MessageListScrollController? controller, + bool reuseController = false, required double topHeight, required double bottomHeight, }) async { + if (!reuseController) { + controller = MessageListScrollController(); + } await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr, child: buildList(controller: controller, topHeight: topHeight, bottomHeight: bottomHeight))); @@ -123,20 +128,18 @@ void main() { }); testWidgets('stick to end of list when it grows', (tester) async { - final controller = MessageListScrollController(); - await prepare(tester, controller: controller, + await prepare(tester, topHeight: 400, bottomHeight: 400); check(tester.getRect(findBottom))..top.equals(200)..bottom.equals(600); // Bottom sliver grows; remain scrolled to (new) bottom. - await prepare(tester, controller: controller, + await prepare(tester, reuseController: true, topHeight: 400, bottomHeight: 500); check(tester.getRect(findBottom))..top.equals(100)..bottom.equals(600); }); testWidgets('when not at end, let it grow without following', (tester) async { - final controller = MessageListScrollController(); - await prepare(tester, controller: controller, + await prepare(tester, topHeight: 400, bottomHeight: 400); check(tester.getRect(findBottom))..top.equals(200)..bottom.equals(600); @@ -146,7 +149,7 @@ void main() { check(tester.getRect(findBottom))..top.equals(300)..bottom.equals(700); // Bottom sliver grows; remain at existing position, now farther from end. - await prepare(tester, controller: controller, + await prepare(tester, reuseController: true, topHeight: 400, bottomHeight: 500); check(tester.getRect(findBottom))..top.equals(300)..bottom.equals(800); }); From 40575610d713d0a873b1061fd5b4fafb995f2206 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 18 Apr 2025 18:07:30 -0700 Subject: [PATCH 12/22] scroll test: Test basic behavior of scroll-to-end feature --- test/widgets/scrolling_test.dart | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/widgets/scrolling_test.dart b/test/widgets/scrolling_test.dart index d86259191b..b07ba5299d 100644 --- a/test/widgets/scrolling_test.dart +++ b/test/widgets/scrolling_test.dart @@ -24,6 +24,7 @@ void main() { } late MessageListScrollController controller; + late MessageListScrollPosition position; Future prepare(WidgetTester tester, { bool reuseController = false, @@ -37,6 +38,7 @@ void main() { child: buildList(controller: controller, topHeight: topHeight, bottomHeight: bottomHeight))); await tester.pump(); + position = controller.position; } // The `skipOffstage: false` produces more informative output @@ -189,5 +191,70 @@ void main() { // … and check the scroll position is preserved, not reset to initial. check(tester.getRect(findTop)).bottom.equals(400); }); + + group('scrollToEnd', () { + testWidgets('short -> slow', (tester) async { + await prepare(tester, topHeight: 300, bottomHeight: 600); + await tester.drag(findBottom, Offset(0, 300)); + await tester.pump(); + check(position.extentAfter).equals(300); + + // Start scrolling to end, from just a short distance up. + position.scrollToEnd(); + await tester.pump(); + check(position.extentAfter).equals(300); + check(position.activity).isA(); + + // The scrolling moves at a stately pace; … + await tester.pump(Duration(milliseconds: 100)); + check(position.extentAfter).equals(200); + + await tester.pump(Duration(milliseconds: 100)); + check(position.extentAfter).equals(100); + + // … then upon reaching the end, … + await tester.pump(Duration(milliseconds: 100)); + check(position.extentAfter).equals(0); + + // … goes idle on the next frame, … + await tester.pump(Duration(milliseconds: 1)); + check(position.activity).isA(); + // … without moving any farther. + check(position.extentAfter).equals(0); + }); + + testWidgets('long -> bounded speed', (tester) async { + const referenceSpeed = 8000.0; + const seconds = 10; + const distance = seconds * referenceSpeed; + await prepare(tester, topHeight: distance + 1000, bottomHeight: 300); + await tester.drag(findBottom, Offset(0, distance)); + await tester.pump(); + check(position.extentAfter).equals(distance); + + // Start scrolling to end. + position.scrollToEnd(); + await tester.pump(); + check(position.activity).isA(); + + // Let it scroll, plotting the trajectory. + final log = []; + for (int i = 0; i < seconds; i++) { + log.add(position.extentAfter); + await tester.pump(const Duration(seconds: 1)); + } + log.add(position.extentAfter); + check(log).deepEquals(List.generate(seconds + 1, + (i) => distance - referenceSpeed * i)); + + // Having reached the end, … + check(position.extentAfter).equals(0); + // … it goes idle on the next frame, … + await tester.pump(Duration(milliseconds: 1)); + check(position.activity).isA(); + // … without moving any farther. + check(position.extentAfter).equals(0); + }); + }); }); } From 26457122aa9844001040666e6b323d8d314a9860 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 5 Apr 2025 21:26:02 -0700 Subject: [PATCH 13/22] scroll [nfc]: Explain magic numbers in scroll-to-end calculations --- lib/widgets/scrolling.dart | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index ed4a674f25..631e4b95c1 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -178,10 +178,28 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext { /// at the start of the animation, even if that ends up being more or less far /// than the actual extent of the content. void scrollToEnd() { + /// The top speed to move at, in logical pixels per second. + /// + /// This will be the speed whenever the distance to be traveled + /// is long enough to take at least [minDuration] at this speed. + /// + /// This is chosen to equal the top speed that can be produced + /// by a fling gesture in a Flutter [ScrollView], + /// which in turn was chosen to equal the top speed of + /// an (initial) fling gesture in a native Android scroll view. + const double topSpeed = 8000; + + /// The desired duration of the animation when traveling short distances. + /// + /// The speed will be chosen so that traveling the distance + /// will take this long, whenever that distance is short enough + /// that that means a speed of at most [topSpeed]. + const minDuration = Duration(milliseconds: 300); + final target = maxScrollExtent; final distance = target - pixels; - final durationMsAtSpeedLimit = (1000 * distance / 8000).ceil(); - final durationMs = math.max(300, durationMsAtSpeedLimit); + final durationMsAtSpeedLimit = (1000 * distance / topSpeed).ceil(); + final durationMs = math.max(minDuration.inMilliseconds, durationMsAtSpeedLimit); animateTo( target, duration: Duration(milliseconds: durationMs), From c778908d994c60628f20c8fbdcec542285ebd158 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 25 Apr 2025 00:05:55 -0700 Subject: [PATCH 14/22] scroll [nfc]: Clarify scroll-to-end calculations a bit more This version keeps the numbers in the form of doubles, with seconds as the unit of time, until the end. That's a bit more typical Flutter style, and also brings it closer to how the logic will look when we flip this around to produce a velocity instead of a duration. --- lib/widgets/scrolling.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index 631e4b95c1..fbf88b171e 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -197,13 +197,11 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext { const minDuration = Duration(milliseconds: 300); final target = maxScrollExtent; - final distance = target - pixels; - final durationMsAtSpeedLimit = (1000 * distance / topSpeed).ceil(); - final durationMs = math.max(minDuration.inMilliseconds, durationMsAtSpeedLimit); - animateTo( - target, - duration: Duration(milliseconds: durationMs), - curve: Curves.linear); + final durationSecAtSpeedLimit = (target - pixels) / topSpeed; + final durationSec = math.max(durationSecAtSpeedLimit, + minDuration.inMilliseconds / 1000.0); + final duration = Duration(milliseconds: (durationSec * 1000.0).ceil()); + animateTo(target, duration: duration, curve: Curves.linear); } } From 717cd04168bf9efba6bacb675c1ef8f86ed98f5c Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 5 Apr 2025 21:41:27 -0700 Subject: [PATCH 15/22] scroll [nfc]: Inline animateTo implementation into scrollToEnd This gets us hands-on control of the ScrollActivity being used, which we'll want to customize in order to get the behavior we want. --- lib/widgets/scrolling.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index fbf88b171e..1867a3eb65 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -178,6 +178,15 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext { /// at the start of the animation, even if that ends up being more or less far /// than the actual extent of the content. void scrollToEnd() { + final target = maxScrollExtent; + + final tolerance = physics.toleranceFor(this); + if (nearEqual(pixels, target, tolerance.distance)) { + // Skip the animation; jump right to the target, which is already close. + jumpTo(target); + return; + } + /// The top speed to move at, in logical pixels per second. /// /// This will be the speed whenever the distance to be traveled @@ -196,12 +205,12 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext { /// that that means a speed of at most [topSpeed]. const minDuration = Duration(milliseconds: 300); - final target = maxScrollExtent; final durationSecAtSpeedLimit = (target - pixels) / topSpeed; final durationSec = math.max(durationSecAtSpeedLimit, minDuration.inMilliseconds / 1000.0); final duration = Duration(milliseconds: (durationSec * 1000.0).ceil()); - animateTo(target, duration: duration, curve: Curves.linear); + beginActivity(DrivenScrollActivity(this, vsync: context.vsync, + from: pixels, to: target, duration: duration, curve: Curves.linear)); } } From 63734b05864837a5797d4658f8e44881f2177944 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 18 Apr 2025 21:06:11 -0700 Subject: [PATCH 16/22] test [nfc]: Organize Flutter checks-extensions by library --- test/flutter_checks.dart | 170 +++++++++++++++++++++++---------------- 1 file changed, 101 insertions(+), 69 deletions(-) diff --git a/test/flutter_checks.dart b/test/flutter_checks.dart index 49e402d158..37932b7ed0 100644 --- a/test/flutter_checks.dart +++ b/test/flutter_checks.dart @@ -7,6 +7,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +//////////////////////////////////////////////////////////////// +// From the Flutter engine, i.e. from dart:ui. +// + extension PaintChecks on Subject { Subject get shader => has((x) => x.shader, 'shader'); } @@ -26,15 +30,89 @@ extension RectChecks on Subject { // TODO others } +extension FontVariationChecks on Subject { + Subject get axis => has((x) => x.axis, 'axis'); + Subject get value => has((x) => x.value, 'value'); +} + +extension SizeChecks on Subject { + Subject get width => has((x) => x.width, 'width'); + Subject get height => has((x) => x.height, 'height'); +} + +//////////////////////////////////////////////////////////////// +// From 'package:flutter/foundation.dart'. +// + +extension ValueListenableChecks on Subject> { + Subject get value => has((c) => c.value, 'value'); +} + +//////////////////////////////////////////////////////////////// +// From 'package:flutter/services.dart'. +// + +extension ClipboardDataChecks on Subject { + Subject get text => has((d) => d.text, 'text'); +} + +extension TextEditingValueChecks on Subject { + Subject get text => has((x) => x.text, 'text'); + Subject get selection => has((x) => x.selection, 'selection'); + Subject get composing => has((x) => x.composing, 'composing'); +} + +//////////////////////////////////////////////////////////////// +// From 'package:flutter/animation.dart'. +// + extension AnimationChecks on Subject> { Subject get status => has((d) => d.status, 'status'); Subject get value => has((d) => d.value, 'value'); } -extension ClipboardDataChecks on Subject { - Subject get text => has((d) => d.text, 'text'); +//////////////////////////////////////////////////////////////// +// From 'package:flutter/painting.dart'. +// + +extension TextStyleChecks on Subject { + Subject get inherit => has((t) => t.inherit, 'inherit'); + Subject get color => has((t) => t.color, 'color'); + Subject get fontSize => has((t) => t.fontSize, 'fontSize'); + Subject get fontWeight => has((t) => t.fontWeight, 'fontWeight'); + Subject get letterSpacing => has((t) => t.letterSpacing, 'letterSpacing'); + Subject?> get fontVariations => has((t) => t.fontVariations, 'fontVariations'); + Subject get fontFamily => has((t) => t.fontFamily, 'fontFamily'); + Subject?> get fontFamilyFallback => has((t) => t.fontFamilyFallback, 'fontFamilyFallback'); + + // TODO others +} + +extension InlineSpanChecks on Subject { + Subject get style => has((x) => x.style, 'style'); +} + +extension BoxDecorationChecks on Subject { + Subject get color => has((x) => x.color, 'color'); +} + +//////////////////////////////////////////////////////////////// +// From 'package:flutter/rendering.dart'. +// + +extension RenderBoxChecks on Subject { + Subject get size => has((x) => x.size, 'size'); } +extension RenderParagraphChecks on Subject { + Subject get text => has((x) => x.text, 'text'); + Subject get didExceedMaxLines => has((x) => x.didExceedMaxLines, 'didExceedMaxLines'); +} + +//////////////////////////////////////////////////////////////// +// From 'package:flutter/widgets.dart'. +// + extension ColoredBoxChecks on Subject { Subject get color => has((d) => d.color, 'color'); } @@ -45,10 +123,6 @@ extension GlobalKeyChecks> on Subject get currentState => has((k) => k.currentState, 'currentState'); } -extension RenderBoxChecks on Subject { - Subject get size => has((x) => x.size, 'size'); -} - extension IconChecks on Subject { Subject get icon => has((i) => i.icon, 'icon'); Subject get color => has((i) => i.color, 'color'); @@ -70,47 +144,41 @@ extension RouteSettingsChecks on Subject { Subject get arguments => has((s) => s.arguments, 'arguments'); } -extension ValueListenableChecks on Subject> { - Subject get value => has((c) => c.value, 'value'); -} - extension TextChecks on Subject { Subject get data => has((t) => t.data, 'data'); Subject get style => has((t) => t.style, 'style'); } -extension TextEditingValueChecks on Subject { - Subject get text => has((x) => x.text, 'text'); - Subject get selection => has((x) => x.selection, 'selection'); - Subject get composing => has((x) => x.composing, 'composing'); -} - extension TextEditingControllerChecks on Subject { Subject get text => has((t) => t.text, 'text'); } -extension TextFieldChecks on Subject { - Subject get textCapitalization => has((t) => t.textCapitalization, 'textCapitalization'); - Subject get decoration => has((t) => t.decoration, 'decoration'); - Subject get controller => has((t) => t.controller, 'controller'); +extension ElementChecks on Subject { + Subject get size => has((t) => t.size, 'size'); + // TODO more } -extension TextStyleChecks on Subject { - Subject get inherit => has((t) => t.inherit, 'inherit'); - Subject get color => has((t) => t.color, 'color'); - Subject get fontSize => has((t) => t.fontSize, 'fontSize'); - Subject get fontWeight => has((t) => t.fontWeight, 'fontWeight'); - Subject get letterSpacing => has((t) => t.letterSpacing, 'letterSpacing'); - Subject?> get fontVariations => has((t) => t.fontVariations, 'fontVariations'); - Subject get fontFamily => has((t) => t.fontFamily, 'fontFamily'); - Subject?> get fontFamilyFallback => has((t) => t.fontFamilyFallback, 'fontFamilyFallback'); +extension MediaQueryDataChecks on Subject { + Subject get textScaler => has((x) => x.textScaler, 'textScaler'); + // TODO more +} - // TODO others +extension TableRowChecks on Subject { + Subject get decoration => has((x) => x.decoration, 'decoration'); } -extension FontVariationChecks on Subject { - Subject get axis => has((x) => x.axis, 'axis'); - Subject get value => has((x) => x.value, 'value'); +extension TableChecks on Subject { + Subject> get children => has((x) => x.children, 'children'); +} + +//////////////////////////////////////////////////////////////// +// From 'package:flutter/material.dart'. +// + +extension TextFieldChecks on Subject { + Subject get textCapitalization => has((t) => t.textCapitalization, 'textCapitalization'); + Subject get decoration => has((t) => t.decoration, 'decoration'); + Subject get controller => has((t) => t.controller, 'controller'); } extension TextThemeChecks on Subject { @@ -139,30 +207,6 @@ extension TypographyChecks on Subject { Subject get tall => has((t) => t.tall, 'tall'); } -extension InlineSpanChecks on Subject { - Subject get style => has((x) => x.style, 'style'); -} - -extension RenderParagraphChecks on Subject { - Subject get text => has((x) => x.text, 'text'); - Subject get didExceedMaxLines => has((x) => x.didExceedMaxLines, 'didExceedMaxLines'); -} - -extension SizeChecks on Subject { - Subject get width => has((x) => x.width, 'width'); - Subject get height => has((x) => x.height, 'height'); -} - -extension ElementChecks on Subject { - Subject get size => has((t) => t.size, 'size'); - // TODO more -} - -extension MediaQueryDataChecks on Subject { - Subject get textScaler => has((x) => x.textScaler, 'textScaler'); - // TODO more -} - extension MaterialChecks on Subject { Subject get color => has((x) => x.color, 'color'); // TODO more @@ -184,18 +228,6 @@ extension ThemeDataChecks on Subject { Subject get brightness => has((x) => x.brightness, 'brightness'); } -extension BoxDecorationChecks on Subject { - Subject get color => has((x) => x.color, 'color'); -} - -extension TableRowChecks on Subject { - Subject get decoration => has((x) => x.decoration, 'decoration'); -} - -extension TableChecks on Subject
{ - Subject> get children => has((x) => x.children, 'children'); -} - extension IconButtonChecks on Subject { Subject get isSelected => has((x) => x.isSelected, 'isSelected'); } From c72b99aadf8aa27a728fd2f27ef33cfd47bf1726 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 18 Apr 2025 21:12:42 -0700 Subject: [PATCH 17/22] test [nfc]: Organize Flutter checks-extensions thematically within each library --- test/flutter_checks.dart | 108 +++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/test/flutter_checks.dart b/test/flutter_checks.dart index 37932b7ed0..f2b20fc41f 100644 --- a/test/flutter_checks.dart +++ b/test/flutter_checks.dart @@ -11,15 +11,16 @@ import 'package:flutter/services.dart'; // From the Flutter engine, i.e. from dart:ui. // -extension PaintChecks on Subject { - Subject get shader => has((x) => x.shader, 'shader'); -} - extension OffsetChecks on Subject { Subject get dx => has((x) => x.dx, 'dx'); Subject get dy => has((x) => x.dy, 'dy'); } +extension SizeChecks on Subject { + Subject get width => has((x) => x.width, 'width'); + Subject get height => has((x) => x.height, 'height'); +} + extension RectChecks on Subject { Subject get left => has((d) => d.left, 'left'); Subject get top => has((d) => d.top, 'top'); @@ -30,16 +31,15 @@ extension RectChecks on Subject { // TODO others } +extension PaintChecks on Subject { + Subject get shader => has((x) => x.shader, 'shader'); +} + extension FontVariationChecks on Subject { Subject get axis => has((x) => x.axis, 'axis'); Subject get value => has((x) => x.value, 'value'); } -extension SizeChecks on Subject { - Subject get width => has((x) => x.width, 'width'); - Subject get height => has((x) => x.height, 'height'); -} - //////////////////////////////////////////////////////////////// // From 'package:flutter/foundation.dart'. // @@ -75,6 +75,10 @@ extension AnimationChecks on Subject> { // From 'package:flutter/painting.dart'. // +extension BoxDecorationChecks on Subject { + Subject get color => has((x) => x.color, 'color'); +} + extension TextStyleChecks on Subject { Subject get inherit => has((t) => t.inherit, 'inherit'); Subject get color => has((t) => t.color, 'color'); @@ -92,10 +96,6 @@ extension InlineSpanChecks on Subject { Subject get style => has((x) => x.style, 'style'); } -extension BoxDecorationChecks on Subject { - Subject get color => has((x) => x.color, 'color'); -} - //////////////////////////////////////////////////////////////// // From 'package:flutter/rendering.dart'. // @@ -113,35 +113,24 @@ extension RenderParagraphChecks on Subject { // From 'package:flutter/widgets.dart'. // -extension ColoredBoxChecks on Subject { - Subject get color => has((d) => d.color, 'color'); -} - extension GlobalKeyChecks> on Subject> { Subject get currentContext => has((k) => k.currentContext, 'currentContext'); Subject get currentWidget => has((k) => k.currentWidget, 'currentWidget'); Subject get currentState => has((k) => k.currentState, 'currentState'); } -extension IconChecks on Subject { - Subject get icon => has((i) => i.icon, 'icon'); - Subject get color => has((i) => i.color, 'color'); - - // TODO others -} - -extension RouteChecks on Subject> { - Subject get isFirst => has((r) => r.isFirst, 'isFirst'); - Subject get settings => has((r) => r.settings, 'settings'); +extension ElementChecks on Subject { + Subject get size => has((t) => t.size, 'size'); + // TODO more } -extension PageRouteChecks on Subject> { - Subject get fullscreenDialog => has((x) => x.fullscreenDialog, 'fullscreenDialog'); +extension MediaQueryDataChecks on Subject { + Subject get textScaler => has((x) => x.textScaler, 'textScaler'); + // TODO more } -extension RouteSettingsChecks on Subject { - Subject get name => has((s) => s.name, 'name'); - Subject get arguments => has((s) => s.arguments, 'arguments'); +extension ColoredBoxChecks on Subject { + Subject get color => has((d) => d.color, 'color'); } extension TextChecks on Subject { @@ -153,14 +142,11 @@ extension TextEditingControllerChecks on Subject { Subject get text => has((t) => t.text, 'text'); } -extension ElementChecks on Subject { - Subject get size => has((t) => t.size, 'size'); - // TODO more -} +extension IconChecks on Subject { + Subject get icon => has((i) => i.icon, 'icon'); + Subject get color => has((i) => i.color, 'color'); -extension MediaQueryDataChecks on Subject { - Subject get textScaler => has((x) => x.textScaler, 'textScaler'); - // TODO more + // TODO others } extension TableRowChecks on Subject { @@ -171,14 +157,27 @@ extension TableChecks on Subject
{ Subject> get children => has((x) => x.children, 'children'); } +extension RouteChecks on Subject> { + Subject get isFirst => has((r) => r.isFirst, 'isFirst'); + Subject get settings => has((r) => r.settings, 'settings'); +} + +extension RouteSettingsChecks on Subject { + Subject get name => has((s) => s.name, 'name'); + Subject get arguments => has((s) => s.arguments, 'arguments'); +} + +extension PageRouteChecks on Subject> { + Subject get fullscreenDialog => has((x) => x.fullscreenDialog, 'fullscreenDialog'); +} + //////////////////////////////////////////////////////////////// // From 'package:flutter/material.dart'. // -extension TextFieldChecks on Subject { - Subject get textCapitalization => has((t) => t.textCapitalization, 'textCapitalization'); - Subject get decoration => has((t) => t.decoration, 'decoration'); - Subject get controller => has((t) => t.controller, 'controller'); +extension MaterialChecks on Subject { + Subject get color => has((x) => x.color, 'color'); + // TODO more } extension TextThemeChecks on Subject { @@ -207,27 +206,28 @@ extension TypographyChecks on Subject { Subject get tall => has((t) => t.tall, 'tall'); } -extension MaterialChecks on Subject { - Subject get color => has((x) => x.color, 'color'); - // TODO more +extension ThemeDataChecks on Subject { + Subject get brightness => has((x) => x.brightness, 'brightness'); } extension InputDecorationChecks on Subject { Subject get hintText => has((x) => x.hintText, 'hintText'); } -extension RadioListTileChecks on Subject> { - Subject get checked => has((x) => x.checked, 'checked'); +extension TextFieldChecks on Subject { + Subject get textCapitalization => has((t) => t.textCapitalization, 'textCapitalization'); + Subject get decoration => has((t) => t.decoration, 'decoration'); + Subject get controller => has((t) => t.controller, 'controller'); } -extension SwitchListTileChecks on Subject { - Subject get value => has((x) => x.value, 'value'); +extension IconButtonChecks on Subject { + Subject get isSelected => has((x) => x.isSelected, 'isSelected'); } -extension ThemeDataChecks on Subject { - Subject get brightness => has((x) => x.brightness, 'brightness'); +extension SwitchListTileChecks on Subject { + Subject get value => has((x) => x.value, 'value'); } -extension IconButtonChecks on Subject { - Subject get isSelected => has((x) => x.isSelected, 'isSelected'); +extension RadioListTileChecks on Subject> { + Subject get checked => has((x) => x.checked, 'checked'); } From 7055b9c72f4a6b45acacc29683ffcc30fe5c3eac Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 18 Apr 2025 20:47:15 -0700 Subject: [PATCH 18/22] scroll: Handle starting from overscroll in "scroll to end" --- lib/widgets/scrolling.dart | 8 +++++++ test/flutter_checks.dart | 4 ++++ test/widgets/scrolling_test.dart | 41 ++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index 1867a3eb65..80bb2c5078 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -187,6 +187,14 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext { return; } + if (pixels > maxScrollExtent) { + // The position is already scrolled past the end. Let overscroll handle it. + // (This situation shouldn't even arise; the UI only offers this option + // when `pixels < maxScrollExtent`.) + goBallistic(0.0); + return; + } + /// The top speed to move at, in logical pixels per second. /// /// This will be the speed whenever the distance to be traveled diff --git a/test/flutter_checks.dart b/test/flutter_checks.dart index f2b20fc41f..328bfdd843 100644 --- a/test/flutter_checks.dart +++ b/test/flutter_checks.dart @@ -142,6 +142,10 @@ extension TextEditingControllerChecks on Subject { Subject get text => has((t) => t.text, 'text'); } +extension ScrollActivityChecks on Subject { + Subject get velocity => has((x) => x.velocity, 'velocity'); +} + extension IconChecks on Subject { Subject get icon => has((i) => i.icon, 'icon'); Subject get color => has((i) => i.color, 'color'); diff --git a/test/widgets/scrolling_test.dart b/test/widgets/scrolling_test.dart index b07ba5299d..4601b96349 100644 --- a/test/widgets/scrolling_test.dart +++ b/test/widgets/scrolling_test.dart @@ -1,4 +1,5 @@ import 'package:checks/checks.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:zulip/widgets/scrolling.dart'; @@ -255,6 +256,46 @@ void main() { // … without moving any farther. check(position.extentAfter).equals(0); }); + + testWidgets('starting from overscroll, just drift', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + await prepare(tester, topHeight: 400, bottomHeight: 400); + + // Drag into overscroll. + await tester.drag(findBottom, Offset(0, -100)); + await tester.pump(); + final offset1 = position.pixels - position.maxScrollExtent; + check(offset1).isGreaterThan(100 / 2); + check(position.activity).isA(); + + // Start drifting back into range. + await tester.pump(Duration(milliseconds: 10)); + final offset2 = position.pixels - position.maxScrollExtent; + check(offset2)..isGreaterThan(0.0)..isLessThan(offset1); + check(position.activity).isA() + .velocity.isLessThan(0); + + // Invoke `scrollToEnd`. The motion should stop… + position.scrollToEnd(); + await tester.pump(); + check(position.pixels - position.maxScrollExtent).equals(offset2); + check(position.activity).isA() + .velocity.equals(0); + + // … and resume drifting from there… + await tester.pump(Duration(milliseconds: 10)); + final offset3 = position.pixels - position.maxScrollExtent; + check(offset3)..isGreaterThan(0.0)..isLessThan(offset2); + check(position.activity).isA() + .velocity.isLessThan(0); + + // … to eventually return to being in range. + await tester.pump(Duration(seconds: 1)); + check(position.pixels - position.maxScrollExtent).equals(0); + check(position.activity).isA(); + + debugDefaultTargetPlatformOverride = null; + }); }); }); } From 6264ed9b64bd4508164fcc0cdf73a6afc07abaa1 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 18 Apr 2025 16:52:58 -0700 Subject: [PATCH 19/22] scroll [nfc]: Introduce ScrollToEndActivity We'll use this to customize the behavior. It also makes a handy marker in tests, when using the testing-only [ScrollPosition.activity] getter for inspecting what's going on in the scroll behavior. --- lib/widgets/scrolling.dart | 17 ++++++++++++++++- test/widgets/scrolling_test.dart | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index 80bb2c5078..9b58a9fd5a 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -43,6 +43,21 @@ class _SingleChildScrollViewWithScrollbarState } } +/// An activity that animates a scroll view smoothly to its end. +/// +/// In particular this drives the "scroll to bottom" button +/// in the Zulip message list. +class ScrollToEndActivity extends DrivenScrollActivity { + ScrollToEndActivity( + super.delegate, { + required super.from, + required super.to, + required super.duration, + required super.curve, + required super.vsync, + }); +} + /// A version of [ScrollPosition] adapted for the Zulip message list, /// used by [MessageListScrollController]. class MessageListScrollPosition extends ScrollPositionWithSingleContext { @@ -217,7 +232,7 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext { final durationSec = math.max(durationSecAtSpeedLimit, minDuration.inMilliseconds / 1000.0); final duration = Duration(milliseconds: (durationSec * 1000.0).ceil()); - beginActivity(DrivenScrollActivity(this, vsync: context.vsync, + beginActivity(ScrollToEndActivity(this, vsync: context.vsync, from: pixels, to: target, duration: duration, curve: Curves.linear)); } } diff --git a/test/widgets/scrolling_test.dart b/test/widgets/scrolling_test.dart index 4601b96349..2a1e94a5bd 100644 --- a/test/widgets/scrolling_test.dart +++ b/test/widgets/scrolling_test.dart @@ -204,7 +204,7 @@ void main() { position.scrollToEnd(); await tester.pump(); check(position.extentAfter).equals(300); - check(position.activity).isA(); + check(position.activity).isA(); // The scrolling moves at a stately pace; … await tester.pump(Duration(milliseconds: 100)); @@ -236,7 +236,7 @@ void main() { // Start scrolling to end. position.scrollToEnd(); await tester.pump(); - check(position.activity).isA(); + check(position.activity).isA(); // Let it scroll, plotting the trajectory. final log = []; From fbd5266baaa30733d3e0ee74cd21952a05f5cd52 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 18 Apr 2025 16:53:10 -0700 Subject: [PATCH 20/22] scroll: Avoid overscroll in "scroll to end" As long as the bottom sliver is size zero (or more generally, as long as maxScrollExtent does not change during the animation), this is nearly NFC: I believe the only changes in behavior would come from differences in rounding. This change handles the case where the end turns out to be closer than it looked at the beginning of the animation. Before this change, the animation would try to scroll past the end in that case. Now it stops at exactly the end -- just like it already did in the case where the end was known exactly in advance, as it currently always is in the actual message list. That case is a possibility as soon as there's a bottom sliver with a message in it: scroll up so the message is offscreen and no longer built; then have the message edited so it becomes shorter; then scroll back down. It's impossible for the viewport to know that the bottom sliver's content has gotten taller until we actually scroll back down and cause the message's widget to get built. In order to customize this behavior, this change uses a feature I added recently (for this purpose) to DrivenScrollActivity upstream: https://github.com/flutter/flutter/pull/166731 --- lib/widgets/scrolling.dart | 25 +++++++++++++++++++++ test/widgets/scrolling_test.dart | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index 9b58a9fd5a..e31969948f 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -56,6 +56,31 @@ class ScrollToEndActivity extends DrivenScrollActivity { required super.curve, required super.vsync, }); + + ScrollPosition get _position => delegate as ScrollPosition; + + @override + bool applyMoveTo(double value) { + bool done = false; + if (value > _position.maxScrollExtent) { + // The activity has reached the end. + // Stop at exactly the end, rather than causing overscroll. + // Possibly some overscroll would actually be desirable, but: + // TODO(upstream) stretch-overscroll seems busted, inverted: + // Is this formula (from [_StretchController.absorbImpact] really right? + // _stretchSizeTween.end = + // math.min(_stretchIntensity + (_flingFriction / velocity), 1.0); + // Seems to take low velocity to the largest stretch, and high velocity + // to the smallest stretch. + // Specifically, a very slow fling produces a very large stretch, + // while other flings produce small stretches that vary little + // between modest speed (~300 px/s) and top speed (8000 px/s). + value = _position.maxScrollExtent; + done = true; + } + if (!super.applyMoveTo(value)) return false; + return !done; + } } /// A version of [ScrollPosition] adapted for the Zulip message list, diff --git a/test/widgets/scrolling_test.dart b/test/widgets/scrolling_test.dart index 2a1e94a5bd..f84adb6972 100644 --- a/test/widgets/scrolling_test.dart +++ b/test/widgets/scrolling_test.dart @@ -296,6 +296,44 @@ void main() { debugDefaultTargetPlatformOverride = null; }); + + testWidgets('on overscroll, stop', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + await prepare(tester, topHeight: 400, bottomHeight: 1000); + + // Scroll up… + position.jumpTo(400); + await tester.pump(); + check(position.extentAfter).equals(600); + + // … then invoke `scrollToEnd`… + position.scrollToEnd(); + await tester.pump(); + + // … but have the bottom sliver turn out to be shorter than it was. + await prepare(tester, topHeight: 400, bottomHeight: 600, + reuseController: true); + check(position.extentAfter).equals(200); + + // Let the scrolling animation proceed until it hits the end. + int steps = 0; + while (position.extentAfter > 0) { + check(++steps).isLessThan(100); + await tester.pump(Duration(milliseconds: 11)); + } + + // This is the very first frame where the position reached the end. + // It's at exactly the end, no farther… + check(position.pixels - position.maxScrollExtent).equals(0); + + // … and the animation is done. Nothing further happens. + check(position.activity).isA(); + await tester.pump(Duration(milliseconds: 11)); + check(position.pixels - position.maxScrollExtent).equals(0); + check(position.activity).isA(); + + debugDefaultTargetPlatformOverride = null; + }); }); }); } From 25b611193b5c00c5702e5a7954afb7750d20079c Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 4 Apr 2025 20:26:44 -0700 Subject: [PATCH 21/22] scroll: Drive "scroll to end" through uncertainty about endpoint As long as the bottom sliver is size zero (or more generally, as long as maxScrollExtent does not change during the animation), this is nearly NFC: I believe the only changes in behavior would come from differences in rounding. By describing the animation in terms of velocity, rather than a duration and exact target position, this lets us smoothly handle the case where we may not know exactly what the position coordinate of the end will be. A previous commit handled the case where the end comes sooner than estimated, by promptly stopping when that happens. This commit ensures the scroll continues past the original estimate, in the case where the end comes later. That case is a possibility as soon as there's a bottom sliver with a message in it: scroll up so the message is offscreen and no longer built; then have the message edited so it becomes taller; then scroll back down. It's impossible for the viewport to know that the bottom sliver's content has gotten taller until we actually scroll back down and cause the message's widget to get built. And naturally that will become even more salient of an issue when we enable the message list to jump into the middle of a long history, so that the bottom sliver may have content that hasn't yet been scrolled to, has never been built as widgets, and may not even have yet been fetched from the server. In order to control the behavior with a Simulation rather than a fixed endpoint and duration with a Curve, this commit uses a feature I added recently for this purpose to DrivenScrollActivity upstream: https://github.com/flutter/flutter/pull/166730 --- lib/widgets/scrolling.dart | 135 +++++++++++++++++++++---------- test/widgets/scrolling_test.dart | 32 ++++++++ 2 files changed, 124 insertions(+), 43 deletions(-) diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index e31969948f..7a7b2cc86e 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -43,19 +43,91 @@ class _SingleChildScrollViewWithScrollbarState } } +/// A simulation of motion at a constant velocity. +/// +/// Models a particle that follows Newton's law of inertia, +/// with no forces acting on the particle, and no end to the motion. +/// +/// See also [GravitySimulation], which adds a constant acceleration +/// and a stopping point. +class InertialSimulation extends Simulation { // TODO(upstream) + InertialSimulation(double initialPosition, double velocity) + : _x0 = initialPosition, _v = velocity; + + final double _x0; + final double _v; + + @override + double x(double time) => _x0 + _v * time; + + @override + double dx(double time) => _v; + + @override + bool isDone(double time) => false; + + @override + String toString() => '${objectRuntimeType(this, 'InertialSimulation')}(' + 'x₀: ${_x0.toStringAsFixed(1)}, dx₀: ${_v.toStringAsFixed(1)})'; +} + +/// A simulation of the user impatiently scrolling to the end of a list. +/// +/// The position [x] is in logical pixels, and time is in seconds. +/// +/// The motion is meant to resemble the user scrolling the list down +/// (by dragging up and flinging), and if the list is long then +/// fling-scrolling again and again to keep it moving quickly. +/// +/// In that scenario taken literally, the motion would repeatedly slow down, +/// then speed up again with a fresh drag and fling. But doing that in +/// response to a simulated drag, as opposed to when the user is actually +/// dragging with their own finger, would feel jerky and not a good UX. +/// Instead this takes a smoothed-out approximation of such a trajectory. +class ScrollToEndSimulation extends InertialSimulation { + factory ScrollToEndSimulation(ScrollPosition position) { + final startPosition = position.pixels; + final estimatedEndPosition = position.maxScrollExtent; + final velocityForMinDuration = (estimatedEndPosition - startPosition) + / (minDuration.inMilliseconds / 1000.0); + assert(velocityForMinDuration > 0); + final velocity = clampDouble(velocityForMinDuration, 0, topSpeed); + return ScrollToEndSimulation._(startPosition, velocity); + } + + ScrollToEndSimulation._(super.initialPosition, super.velocity); + + /// The top speed to move at, in logical pixels per second. + /// + /// This will be the speed whenever the estimated distance to be traveled + /// is long enough to take at least [minDuration] at this speed. + /// + /// This is chosen to equal the top speed that can be produced + /// by a fling gesture in a Flutter [ScrollView], + /// which in turn was chosen to equal the top speed of + /// an (initial) fling gesture in a native Android scroll view. + static const double topSpeed = 8000; + + /// The desired duration of the animation when traveling short distances. + /// + /// The speed will be chosen so that traveling the estimated distance + /// will take this long, whenever that distance is short enough + /// that that means a speed of at most [topSpeed]. + static const minDuration = Duration(milliseconds: 300); +} + /// An activity that animates a scroll view smoothly to its end. /// /// In particular this drives the "scroll to bottom" button /// in the Zulip message list. class ScrollToEndActivity extends DrivenScrollActivity { - ScrollToEndActivity( - super.delegate, { - required super.from, - required super.to, - required super.duration, - required super.curve, - required super.vsync, - }); + /// Create an activity that animates a scroll view smoothly to its end. + /// + /// The [delegate] is required to also implement [ScrollPosition]. + ScrollToEndActivity(ScrollActivityDelegate delegate) + : super.simulation(delegate, + vsync: (delegate as ScrollPosition).context.vsync, + ScrollToEndSimulation(delegate as ScrollPosition)); ScrollPosition get _position => delegate as ScrollPosition; @@ -210,20 +282,20 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext { /// Scroll the position smoothly to the end of the scrollable content. /// - /// This method only works well if [maxScrollExtent] is accurate - /// and does not change during the animation. - /// (For example, this works if there is no content in forward slivers, - /// so that [maxScrollExtent] is always zero.) - /// The animation will attempt to travel to the value [maxScrollExtent] had - /// at the start of the animation, even if that ends up being more or less far - /// than the actual extent of the content. + /// This is similar to calling [animateTo] with a target of [maxScrollExtent], + /// except that if [maxScrollExtent] changes over the course of the animation + /// (for example due to more content being added at the end, + /// or due to the estimated length of the content changing as + /// different items scroll into the viewport), + /// this animation will carry on until it reaches the updated value + /// of [maxScrollExtent], not the value it had at the start of the animation. + /// + /// The animation is typically handled by a [ScrollToEndActivity]. void scrollToEnd() { - final target = maxScrollExtent; - final tolerance = physics.toleranceFor(this); - if (nearEqual(pixels, target, tolerance.distance)) { + if (nearEqual(pixels, maxScrollExtent, tolerance.distance)) { // Skip the animation; jump right to the target, which is already close. - jumpTo(target); + jumpTo(maxScrollExtent); return; } @@ -235,30 +307,7 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext { return; } - /// The top speed to move at, in logical pixels per second. - /// - /// This will be the speed whenever the distance to be traveled - /// is long enough to take at least [minDuration] at this speed. - /// - /// This is chosen to equal the top speed that can be produced - /// by a fling gesture in a Flutter [ScrollView], - /// which in turn was chosen to equal the top speed of - /// an (initial) fling gesture in a native Android scroll view. - const double topSpeed = 8000; - - /// The desired duration of the animation when traveling short distances. - /// - /// The speed will be chosen so that traveling the distance - /// will take this long, whenever that distance is short enough - /// that that means a speed of at most [topSpeed]. - const minDuration = Duration(milliseconds: 300); - - final durationSecAtSpeedLimit = (target - pixels) / topSpeed; - final durationSec = math.max(durationSecAtSpeedLimit, - minDuration.inMilliseconds / 1000.0); - final duration = Duration(milliseconds: (durationSec * 1000.0).ceil()); - beginActivity(ScrollToEndActivity(this, vsync: context.vsync, - from: pixels, to: target, duration: duration, curve: Curves.linear)); + beginActivity(ScrollToEndActivity(this)); } } diff --git a/test/widgets/scrolling_test.dart b/test/widgets/scrolling_test.dart index f84adb6972..3b6e0d10db 100644 --- a/test/widgets/scrolling_test.dart +++ b/test/widgets/scrolling_test.dart @@ -334,6 +334,38 @@ void main() { debugDefaultTargetPlatformOverride = null; }); + + testWidgets('keep going even if content turns out longer', (tester) async { + await prepare(tester, topHeight: 1000, bottomHeight: 3000); + + // Scroll up… + position.jumpTo(0); + await tester.pump(); + check(position.extentAfter).equals(3000); + + // … then invoke `scrollToEnd`… + position.scrollToEnd(); + await tester.pump(); + + // … but have the bottom sliver turn out to be longer than it was. + await prepare(tester, topHeight: 1000, bottomHeight: 6000, + reuseController: true); + check(position.extentAfter).equals(6000); + + // Let the scrolling animation go until it stops. + int steps = 0; + double prevRemaining; + double remaining = position.extentAfter; + do { + prevRemaining = remaining; + check(++steps).isLessThan(100); + await tester.pump(Duration(milliseconds: 10)); + remaining = position.extentAfter; + } while (remaining < prevRemaining); + + // The scroll position should be all the way at the end. + check(remaining).equals(0); + }); }); }); } From ca0abe5a77d88395537392b8897feecacca74666 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 5 Apr 2025 23:03:47 -0700 Subject: [PATCH 22/22] scroll: Apply a min velocity in ScrollToEndSimulation This makes it possible to see in a self-contained way, in this class's own code, that it always starts moving at a velocity that isn't zero, or less than zero, or at risk of being conflated with zero. This doesn't have a big effect in practice, because the only call site already does something else whenever the distance to travel is negative or very close to zero. But there is a small range -- namely where the distance to travel is between 1 and 12 physical pixels, given the default behavior of ScrollPhysics.toleranceFor -- in which this minimum speed does apply. --- lib/widgets/scrolling.dart | 10 ++++++++-- test/widgets/scrolling_test.dart | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/widgets/scrolling.dart b/lib/widgets/scrolling.dart index 7a7b2cc86e..5bccc680a5 100644 --- a/lib/widgets/scrolling.dart +++ b/lib/widgets/scrolling.dart @@ -86,12 +86,18 @@ class InertialSimulation extends Simulation { // TODO(upstream) /// Instead this takes a smoothed-out approximation of such a trajectory. class ScrollToEndSimulation extends InertialSimulation { factory ScrollToEndSimulation(ScrollPosition position) { + final tolerance = position.physics.toleranceFor(position); final startPosition = position.pixels; final estimatedEndPosition = position.maxScrollExtent; final velocityForMinDuration = (estimatedEndPosition - startPosition) / (minDuration.inMilliseconds / 1000.0); - assert(velocityForMinDuration > 0); - final velocity = clampDouble(velocityForMinDuration, 0, topSpeed); + final velocity = clampDouble(velocityForMinDuration, + // If the starting position is beyond the estimated end + // (i.e. `velocityForMinDuration < 0`), or very close to it, + // then move forward at a small positive velocity. + // Let the overscroll handling bring the position to exactly the end. + 2 * tolerance.velocity, + topSpeed); return ScrollToEndSimulation._(startPosition, velocity); } diff --git a/test/widgets/scrolling_test.dart b/test/widgets/scrolling_test.dart index 3b6e0d10db..8e0afafef4 100644 --- a/test/widgets/scrolling_test.dart +++ b/test/widgets/scrolling_test.dart @@ -297,6 +297,27 @@ void main() { debugDefaultTargetPlatformOverride = null; }); + testWidgets('starting very near end, apply min speed', (tester) async { + await prepare(tester, topHeight: 400, bottomHeight: 400); + // Verify the assumption used for constructing the example numbers below. + check(position.physics.toleranceFor(position).velocity) + .isCloseTo(20/3, .01); + + position.jumpTo(398); + await tester.pump(); + check(position.extentAfter).equals(2); + + position.scrollToEnd(); + await tester.pump(); + check(position.extentAfter).equals(2); + + // Reach the end in just 150ms, not 300ms. + await tester.pump(Duration(milliseconds: 75)); + check(position.extentAfter).equals(1); + await tester.pump(Duration(milliseconds: 75)); + check(position.extentAfter).equals(0); + }); + testWidgets('on overscroll, stop', (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; await prepare(tester, topHeight: 400, bottomHeight: 1000);