diff --git a/src/app/browser/friend/index.tsx b/src/app/browser/friend/index.tsx
index 5570483..776915e 100644
--- a/src/app/browser/friend/index.tsx
+++ b/src/app/browser/friend/index.tsx
@@ -8,7 +8,7 @@ import { getTitleIdFromEcUrl, hrduration } from '../../../util/misc.js';
import { Button } from '../components/index.js';
import { DEFAULT_ACCENT_COLOUR, TEXT_COLOUR_ACTIVE, TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js';
import ipc, { events } from '../ipc.js';
-import { RequestState, Root, useAccentColour, useAsync, useColourScheme, useDiscordPresenceSource, useEventListener } from '../util.js';
+import { RequestState, Root, useAccentColour, useAsync, useColourScheme, useDiscordPresenceSource, useEventListener, useTimeSince } from '../util.js';
export interface FriendProps {
user: string;
@@ -86,6 +86,8 @@ export default function Friend(props: FriendProps) {
Friends since {new Date(friend.friendCreatedAt * 1000).toLocaleString('en-GB')}
{friend.presence.updatedAt ? Presence updated at {new Date(friend.presence.updatedAt * 1000).toLocaleString('en-GB')} : null}
+ {!(friend.presence.state === PresenceState.ONLINE || friend.presence.state === PresenceState.PLAYING) &&
+ friend.presence.logoutAt ? Last online at {new Date(friend.presence.logoutAt * 1000).toLocaleString('en-GB')} : null}
This user {can_see_user_presence ? 'can' : 'can not'} see your presence.
@@ -120,6 +122,7 @@ function FriendPresence(props: {
const theme = useColourScheme() === 'light' ? light : dark;
const logout = props.presence.logoutAt ? new Date(props.presence.logoutAt * 1000) : null;
+ const since_logout = useTimeSince(logout ?? new Date(0));
const game = 'name' in props.presence.game ? props.presence.game : null;
if (props.presence.state === PresenceState.ONLINE || props.presence.state === PresenceState.PLAYING) {
@@ -128,7 +131,7 @@ function FriendPresence(props: {
return
Offline
- {logout ? Last seen {logout.toLocaleString('en-GB')} : null}
+ {logout ? Last seen {since_logout} : null}
;
}
diff --git a/src/app/browser/main/friends.tsx b/src/app/browser/main/friends.tsx
index 757c448..a88ef38 100644
--- a/src/app/browser/main/friends.tsx
+++ b/src/app/browser/main/friends.tsx
@@ -1,7 +1,7 @@
import React, { useCallback } from 'react';
import { Image, ImageStyle, Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import ipc from '../ipc.js';
-import { useAccentColour, useColourScheme, User } from '../util.js';
+import { useAccentColour, useColourScheme, User, useTimeSince } from '../util.js';
import { Friend, Presence, PresenceState } from '../../../api/coral-types.js';
import { TEXT_COLOUR_ACTIVE, TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js';
import Section, { HEADER_SIZE } from './section.js';
@@ -95,11 +95,14 @@ function FriendPresence(props: {
}) {
const theme = useColourScheme() === 'light' ? light : dark;
+ const logout = props.presence.logoutAt ? new Date(props.presence.logoutAt * 1000) : null;
+ const since_logout = useTimeSince(logout ?? new Date(0), true);
+
if (props.presence.state === PresenceState.ONLINE || props.presence.state === PresenceState.PLAYING) {
return Playing;
}
- return Offline;
+ return {logout ? since_logout : 'Offline'};
}
const styles = StyleSheet.create({
diff --git a/src/app/browser/util.tsx b/src/app/browser/util.tsx
index 08be69d..bef9088 100644
--- a/src/app/browser/util.tsx
+++ b/src/app/browser/util.tsx
@@ -249,3 +249,54 @@ export function useActiveDiscordUser() {
return user;
}
+
+export function useTimeSince(time: Date, short = false) {
+ const [now, setNow] = useState(Date.now());
+
+ const [since, update_in] = getTimeSince(time, now, short ? short_time_since_intervals : time_since_intervals);
+ const update_at = Date.now() + update_in;
+
+ useEffect(() => {
+ const timeout = setTimeout(() => setNow(Date.now()), Math.max(1000, update_at - Date.now()));
+ return () => clearTimeout(timeout);
+ }, [time, short, update_at]);
+
+ return since;
+}
+
+interface TimeSinceInterval {
+ interval: number;
+ max: number;
+ string: (count: number) => string;
+}
+
+const time_since_intervals: TimeSinceInterval[] = [
+ {interval: 1000, max: 10, string: () => 'just now'},
+ {interval: 1000, max: 60, string: c => c + ' second' + (c === 1 ? '' : 's') + ' ago'},
+ {interval: 60 * 1000, max: 60, string: c => c + ' minute' + (c === 1 ? '' : 's') + ' ago'},
+ {interval: 60 * 60 * 1000, max: 24, string: c => c + ' hour' + (c === 1 ? '' : 's') + ' ago'},
+ {interval: 24 * 60 * 60 * 1000, max: Infinity, string: c => c + ' day' + (c === 1 ? '' : 's') + ' ago'},
+];
+const short_time_since_intervals: TimeSinceInterval[] = [
+ {interval: 1000, max: 10, string: () => 'Just now'},
+ {interval: 1000, max: 60, string: c => c + ' sec' + (c === 1 ? '' : 's')},
+ {interval: 60 * 1000, max: 60, string: c => c + ' min' + (c === 1 ? '' : 's')},
+ {interval: 60 * 60 * 1000, max: 24, string: c => c + ' hr' + (c === 1 ? '' : 's')},
+ {interval: 24 * 60 * 60 * 1000, max: Infinity, string: c => c + ' day' + (c === 1 ? '' : 's')},
+];
+
+function getTimeSince(time: Date | number, now = Date.now(), intervals = time_since_intervals): [string, number] {
+ if (time instanceof Date) time = time.getTime();
+
+ const elapsed = Math.max(0, now - time);
+ const last = intervals[time_since_intervals.length - 1];
+
+ for (const i of intervals) {
+ if (elapsed < i.max * i.interval || last === i) {
+ const count = Math.floor(elapsed / i.interval);
+ return [i.string.call(null, count), i.interval - (elapsed - (count * i.interval))];
+ }
+ }
+
+ throw new Error('Invalid intervals');
+}