diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 77200c01eb..65317771bd 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -851,6 +851,50 @@ "@yesterday": { "description": "Term to use to reference the previous day." }, + "userActiveNow": "Active now", + "@userActiveNow": { + "description": "Indicates a user is currently active on Zulip (not idle or offline)" + }, + "userIdle": "Idle", + "@userIdle": { + "description": "Indicates a user is currently idle on Zulip (not active, but not offline)" + }, + "userActiveMinutesAgo": "Active {minutes, plural, =1{1 minute} other{{minutes} minutes}} ago", + "@userActiveMinutesAgo": { + "description": "Indicates when a user was last active on Zulip (who is currently offline)", + "placeholders": { + "minutes": {"type": "int", "example": "5"} + } + }, + "userActiveHoursAgo": "Active {hours, plural, =1{1 hour} other{{hours} hours}} ago", + "@userActiveHoursAgo": { + "description": "Indicates when a user was last active on Zulip (who is currently offline)", + "placeholders": { + "hours": {"type": "int", "example": "5"} + } + }, + "userActiveYesterday": "Active yesterday", + "@userActiveYesterday": { + "description": "Indicates when a user was last active on Zulip (who is currently offline)" + }, + "userActiveDaysAgo": "Active {days, plural, =1{1 day} other{{days} days}} ago", + "@userActiveDaysAgo": { + "description": "Indicates when a user was last active on Zulip (who is currently offline)", + "placeholders": { + "days": {"type": "int", "example": "5"} + } + }, + "userActiveDate": "Active {date}", + "@userActiveDate": { + "description": "Indicates the date when a user was last active on Zulip (who is currently offline).\n\nThe date might be day and month if recent, or day, month, and year if less recent.", + "placeholders": { + "date": {"type": "String", "example": "Aug 1, 2024"} + } + }, + "userNotActiveInYear": "Not active in the last year", + "@userNotActiveInYear": { + "description": "Indicates when a user was last active on Zulip (who is currently offline)" + }, "invisibleMode": "Invisible mode", "@invisibleMode": { "description": "Label for the 'Invisible mode' switch on the profile page." diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index d7e9c25f8f..32349d4b54 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -1271,6 +1271,56 @@ abstract class ZulipLocalizations { /// **'Yesterday'** String get yesterday; + /// Indicates a user is currently active on Zulip (not idle or offline) + /// + /// In en, this message translates to: + /// **'Active now'** + String get userActiveNow; + + /// Indicates a user is currently idle on Zulip (not active, but not offline) + /// + /// In en, this message translates to: + /// **'Idle'** + String get userIdle; + + /// Indicates when a user was last active on Zulip (who is currently offline) + /// + /// In en, this message translates to: + /// **'Active {minutes, plural, =1{1 minute} other{{minutes} minutes}} ago'** + String userActiveMinutesAgo(int minutes); + + /// Indicates when a user was last active on Zulip (who is currently offline) + /// + /// In en, this message translates to: + /// **'Active {hours, plural, =1{1 hour} other{{hours} hours}} ago'** + String userActiveHoursAgo(int hours); + + /// Indicates when a user was last active on Zulip (who is currently offline) + /// + /// In en, this message translates to: + /// **'Active yesterday'** + String get userActiveYesterday; + + /// Indicates when a user was last active on Zulip (who is currently offline) + /// + /// In en, this message translates to: + /// **'Active {days, plural, =1{1 day} other{{days} days}} ago'** + String userActiveDaysAgo(int days); + + /// Indicates the date when a user was last active on Zulip (who is currently offline). + /// + /// The date might be day and month if recent, or day, month, and year if less recent. + /// + /// In en, this message translates to: + /// **'Active {date}'** + String userActiveDate(String date); + + /// Indicates when a user was last active on Zulip (who is currently offline) + /// + /// In en, this message translates to: + /// **'Not active in the last year'** + String get userNotActiveInYear; + /// Label for the 'Invisible mode' switch on the profile page. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart index 5ff9981001..a49c02d16d 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -698,6 +698,56 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get yesterday => 'Yesterday'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index 8d0826bac9..c000f4bc12 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -719,6 +719,56 @@ class ZulipLocalizationsDe extends ZulipLocalizations { @override String get yesterday => 'Gestern'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index bffdb9f9a9..ace1852f24 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -698,6 +698,56 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get yesterday => 'Yesterday'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index 5001c79eed..0f4d3419cf 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -698,6 +698,56 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get yesterday => 'Yesterday'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index 25a5c4e999..c96393d21c 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -713,6 +713,56 @@ class ZulipLocalizationsIt extends ZulipLocalizations { @override String get yesterday => 'Ieri'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index e39ebe4377..e5a58a37e9 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -695,6 +695,56 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get yesterday => 'Yesterday'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index d08ca0eaf0..18c9168795 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -698,6 +698,56 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get yesterday => 'Yesterday'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index 9b856a6aae..30110727ea 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -709,6 +709,56 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get yesterday => 'Wczoraj'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Tryb ukrycia'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 848b02eefb..1e8e78f7b2 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -712,6 +712,56 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get yesterday => 'Вчера'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Режим невидимости'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 96e9e0c542..5c2dc836b8 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -700,6 +700,56 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get yesterday => 'Včera'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index bdb56cf44d..e8bea0ae06 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -724,6 +724,56 @@ class ZulipLocalizationsSl extends ZulipLocalizations { @override String get yesterday => 'Včeraj'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index 0c2f49363e..fe48d8cacb 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -712,6 +712,56 @@ class ZulipLocalizationsUk extends ZulipLocalizations { @override String get yesterday => 'Учора'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index fdfd2966b5..285501bd0e 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -698,6 +698,56 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get yesterday => 'Yesterday'; + @override + String get userActiveNow => 'Active now'; + + @override + String get userIdle => 'Idle'; + + @override + String userActiveMinutesAgo(int minutes) { + String _temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '$minutes minutes', + one: '1 minute', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveHoursAgo(int hours) { + String _temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '$hours hours', + one: '1 hour', + ); + return 'Active $_temp0 ago'; + } + + @override + String get userActiveYesterday => 'Active yesterday'; + + @override + String userActiveDaysAgo(int days) { + String _temp0 = intl.Intl.pluralLogic( + days, + locale: localeName, + other: '$days days', + one: '1 day', + ); + return 'Active $_temp0 ago'; + } + + @override + String userActiveDate(String date) { + return 'Active $date'; + } + + @override + String get userNotActiveInYear => 'Not active in the last year'; + @override String get invisibleMode => 'Invisible mode'; diff --git a/lib/model/presence.dart b/lib/model/presence.dart index 590c2d3ceb..1c24f1e546 100644 --- a/lib/model/presence.dart +++ b/lib/model/presence.dart @@ -143,6 +143,31 @@ class Presence extends HasRealmStore with ChangeNotifier { } } + /// The timestamp when the given user was "last active", if any. + /// + /// This is meaningful only when [presenceStatusForUser] is null. + /// When that method returns active or idle, the user should be displayed + /// with a description like "Active now" or "Idle" rather than + /// one like "Last active $duration ago" that uses this timestamp. + int? userLastActive(int userId) { + // The corresponding implementation on web is complicated; + // but the actual behavior seems to be this simple. + // + // In web, see buddy_data.user_last_seen_time_status; the last-active time + // is used only when the status is offline (vs active or idle). + // The timestamp comes via presence.last_active_date from the data structure + // fed by presence.status_from_raw. + // + // That status_from_raw function sometimes uses idle_timestamp; + // but only when status idle, where the timestamp will be ignored anyway. + // It also consults the equivalent of [User.dateJoined] as a fallback, + // for when processing a user who has "never logged in"... but it's + // not clear that function ever gets called in such a case. + // Those wrinkles aside, it always uses active_timestamp. + + return _map[userId]?.activeTimestamp; + } + void handlePresenceEvent(PresenceEvent event) { // TODO(#1618) } diff --git a/lib/model/store.dart b/lib/model/store.dart index 4f3e72442f..070c20fb27 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -814,7 +814,8 @@ class PerAccountStore extends PerAccountStoreBase with typingStatus.handleTypingEvent(event); case PresenceEvent(): - // TODO handle + assert(debugLog("server event: presence ${event.userId}")); + // TODO(#1618) handle break; case ReactionEvent(): diff --git a/lib/widgets/profile.dart b/lib/widgets/profile.dart index 461b2ec1b7..423a5be905 100644 --- a/lib/widgets/profile.dart +++ b/lib/widgets/profile.dart @@ -1,13 +1,16 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import '../api/model/model.dart'; import '../api/route/settings.dart'; import '../generated/l10n/zulip_localizations.dart'; import '../log.dart'; +import '../model/binding.dart'; import '../model/content.dart'; import '../model/narrow.dart'; +import '../model/store.dart'; import 'app_bar.dart'; import 'button.dart'; import 'content.dart'; @@ -42,7 +45,6 @@ class ProfilePage extends StatelessWidget { page: ProfilePage(userId: userId)); } - @override Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); @@ -56,7 +58,9 @@ class ProfilePage extends StatelessWidget { .merge(weightVariableTextStyle(context, wght: 700)); final userStatus = store.getUserStatus(userId); + final displayEmail = user.deliveryEmail; + final items = [ Center( child: Avatar( @@ -93,6 +97,9 @@ class ProfilePage extends StatelessWidget { textAlign: TextAlign.center, style: TextStyle(fontSize: 18, height: 22 / 18, color: DesignVariables.of(context).userStatusText)), + _LastActiveTime(userId: userId), + + const SizedBox(height: 8), if (displayEmail != null) Text(displayEmail, textAlign: TextAlign.center, @@ -139,6 +146,72 @@ class ProfilePage extends StatelessWidget { } } +class _LastActiveTime extends StatelessWidget { + const _LastActiveTime({required this.userId}); + + final int userId; + + String _lastActiveText(PerAccountStore store, ZulipLocalizations zulipLocalizations) { + final nowDate = ZulipBinding.instance.utcNow(); + final status = store.presence.presenceStatusForUser(userId, utcNow: nowDate); + switch (status) { + case PresenceStatus.active: return zulipLocalizations.userActiveNow; + case PresenceStatus.idle: return zulipLocalizations.userIdle; + case null: break; // handle below + } + + final timestamp = store.presence.userLastActive(userId); + if (timestamp == null) return zulipLocalizations.userNotActiveInYear; + + // Compare web's timerender.last_seen_status_from_date. + final now = nowDate.millisecondsSinceEpoch ~/ 1000; + final ageSeconds = now - timestamp; + if (ageSeconds <= 0) { + return zulipLocalizations.userActiveNow; + } else if (ageSeconds < 60 * 60) { + return zulipLocalizations.userActiveMinutesAgo(ageSeconds ~/ 60); + } else if (ageSeconds < 24 * 60 * 60) { + return zulipLocalizations.userActiveHoursAgo(ageSeconds ~/ (60 * 60)); + } + + final todayNoon = nowDate.toLocal() + .copyWith(hour: 12, minute: 0, second: 0, millisecond: 0, microsecond: 0); + final presenceNoon = DateTime.fromMillisecondsSinceEpoch( + timestamp, isUtc: false) + .copyWith(hour: 12, minute: 0, second: 0, millisecond: 0, microsecond: 0); + final ageCalendarDays = (todayNoon.difference(presenceNoon) + .inSeconds / 24 * 60 * 60).round(); + if (ageCalendarDays <= 0) { + // The timestamp was at least 24 hours ago. + // If it's somehow the same or a future calendar day, then this must be a + // really messy time zone. Hopefully no real time zone makes this possible. + return zulipLocalizations.userActiveYesterday; + } else if (ageCalendarDays == 1) { + return zulipLocalizations.userActiveYesterday; + } else if (ageCalendarDays < 90) { + return zulipLocalizations.userActiveDaysAgo(ageCalendarDays); + } + + final DateFormat format; + if (presenceNoon.year == todayNoon.year) { + format = DateFormat.MMMd(); + } else { + format = DateFormat.yMMMd(); + } + return zulipLocalizations.userActiveDate(format.format(presenceNoon)); + } + + @override + Widget build(BuildContext context) { + final zulipLocalizations = ZulipLocalizations.of(context); + final store = PerAccountStoreWidget.of(context); + return Text(_lastActiveText(store, zulipLocalizations), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, height: 22 / 18, + color: DesignVariables.of(context).userStatusText)); + } +} + class _SetStatusButton extends StatelessWidget { const _SetStatusButton();