Show online users on desktop

This commit is contained in:
Ian Philips 2022-07-15 08:45:52 -06:00
parent 2610f32521
commit dd9d24e657
6 changed files with 96 additions and 22 deletions

View File

@ -38,6 +38,7 @@ export type User = {
referredByUserId?: string
referredByContractId?: string
lastPingTime?: number
}
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
@ -57,7 +58,6 @@ export type PrivateUser = {
initialIpAddress?: string
apiKey?: string
notificationPreferences?: notification_subscribe_types
lastTimeCheckedBonuses?: number
}
export type notification_subscribe_types = 'all' | 'less' | 'none'

View File

@ -20,16 +20,17 @@ service cloud.firestore {
allow read;
allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']);
allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['referredByUserId'])
// only one referral allowed per user
&& !("referredByUserId" in resource.data)
// user can't refer themselves
&& !(resource.data.id == request.resource.data.referredByUserId);
// quid pro quos enabled (only once though so nbd) - bc I can't make this work:
// && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id);
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId', 'lastPingTime']);
// User referral rules
allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['referredByUserId'])
// only one referral allowed per user
&& !("referredByUserId" in resource.data)
// user can't refer themselves
&& !(resource.data.id == request.resource.data.referredByUserId);
// quid pro quos enabled (only once though so nbd) - bc I can't make this work:
// && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id);
}
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {

View File

@ -7,6 +7,7 @@ import { FollowButton } from './follow-button'
import { Col } from './layout/col'
import { Row } from './layout/row'
import { UserLink } from './user-page'
import { OnlineUserAvatar } from 'web/components/online-user-list'
export function FollowList(props: { userIds: string[] }) {
const { userIds } = props
@ -63,10 +64,7 @@ function UserFollowItem(props: {
return (
<Row className={clsx('items-center justify-between gap-2 p-2', className)}>
<Row className="items-center gap-2">
<Avatar username={user?.username} avatarUrl={user?.avatarUrl} />
{user && <UserLink name={user.name} username={user.username} />}
</Row>
<OnlineUserAvatar user={user} />
{!hideFollowButton && (
<FollowButton
isFollowing={isFollowing}

View File

@ -13,7 +13,7 @@ import clsx from 'clsx'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { firebaseLogout, User } from 'web/lib/firebase/users'
import { firebaseLogout, updateUser, User } from 'web/lib/firebase/users'
import { ManifoldLogo } from './manifold-logo'
import { MenuButton } from './menu'
import { ProfileSummary } from './profile-menu'
@ -208,6 +208,17 @@ export default function Sidebar(props: { className?: string }) {
href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`,
}))
useEffect(() => {
if (!user) return
// set ping time to now every 60 seconds to indicate that the user is active
const pingInterval = setInterval(() => {
updateUser(user.id, {
lastPingTime: Date.now(),
})
}, 1000 * 30)
return () => clearInterval(pingInterval)
}, [user])
return (
<nav aria-label="Sidebar" className={className}>
<ManifoldLogo className="py-6" twoLine />

View File

@ -0,0 +1,56 @@
import clsx from 'clsx'
import { Avatar } from './avatar'
import { Col } from './layout/col'
import { Row } from './layout/row'
import { UserLink } from './user-page'
import { User } from 'common/user'
import { UserCircleIcon } from '@heroicons/react/solid'
import { useUsers } from 'web/hooks/use-users'
import { partition } from 'lodash'
const isOnline = (user?: User) =>
user && user.lastPingTime && user.lastPingTime > Date.now() - 5 * 60 * 1000
export function OnlineUserList(props: { users: User[] }) {
let { users } = props
const liveUsers = useUsers().filter((user) =>
users.map((u) => u.id).includes(user.id)
)
if (liveUsers) users = liveUsers
const [onlineUsers, offlineUsers] = partition(users, (user) => isOnline(user))
return (
<Col className="mt-4 gap-1">
{onlineUsers
.concat(offlineUsers)
.slice(0, 15)
.map((user) => (
<Row
key={user.id}
className={clsx('items-center justify-between gap-2 p-2')}
>
<OnlineUserAvatar key={user.id} user={user} />
</Row>
))}
</Col>
)
}
export function OnlineUserAvatar(props: { user?: User; className?: string }) {
const { user, className } = props
return (
<Row className={clsx('relative items-center gap-2', className)}>
<Avatar
username={user?.username}
avatarUrl={user?.avatarUrl}
className={className}
/>
{user && <UserLink name={user.name} username={user.username} />}
{isOnline(user) && (
<div className="absolute left-0 top-0 ">
<UserCircleIcon className="text-primary bg-primary h-3 w-3 rounded-full border-2 border-white" />
</div>
)}
</Row>
)
}

View File

@ -55,6 +55,7 @@ import { FollowList } from 'web/components/follow-list'
import { SearchIcon } from '@heroicons/react/outline'
import { useTipTxns } from 'web/hooks/use-tip-txns'
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
import { OnlineUserList } from 'web/components/online-user-list'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
@ -174,7 +175,12 @@ export default function GroupPage(props: {
const rightSidebar = (
<Col className="mt-6 hidden xl:block">
<JoinOrCreateButton group={group} user={user} isMember={!!isMember} />
<JoinOrAddQuestionsButtons
group={group}
user={user}
isMember={!!isMember}
/>
<OnlineUserList users={members} />
</Col>
)
const leaderboard = (
@ -254,7 +260,6 @@ export default function GroupPage(props: {
description={`Created by ${creator.name}. ${group.about}`}
url={groupPath(group.slug)}
/>
<Col className="px-3">
<Row className={'items-center justify-between gap-4'}>
<div className={'sm:mb-1'}>
@ -270,7 +275,7 @@ export default function GroupPage(props: {
</div>
</div>
<div className="hidden sm:block xl:hidden">
<JoinOrCreateButton
<JoinOrAddQuestionsButtons
group={group}
user={user}
isMember={!!isMember}
@ -278,10 +283,13 @@ export default function GroupPage(props: {
</div>
</Row>
<div className="block sm:hidden">
<JoinOrCreateButton group={group} user={user} isMember={!!isMember} />
<JoinOrAddQuestionsButtons
group={group}
user={user}
isMember={!!isMember}
/>
</div>
</Col>
<Tabs
currentPageForAnalytics={groupPath(group.slug)}
className={'mb-0 sm:mb-2'}
@ -292,7 +300,7 @@ export default function GroupPage(props: {
)
}
function JoinOrCreateButton(props: {
function JoinOrAddQuestionsButtons(props: {
group: Group
user: User | null | undefined
isMember: boolean