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'); +}