Show online users on desktop
This commit is contained in:
parent
2610f32521
commit
dd9d24e657
|
@ -38,6 +38,7 @@ export type User = {
|
||||||
|
|
||||||
referredByUserId?: string
|
referredByUserId?: string
|
||||||
referredByContractId?: string
|
referredByContractId?: string
|
||||||
|
lastPingTime?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
|
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
|
||||||
|
@ -57,7 +58,6 @@ export type PrivateUser = {
|
||||||
initialIpAddress?: string
|
initialIpAddress?: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
notificationPreferences?: notification_subscribe_types
|
notificationPreferences?: notification_subscribe_types
|
||||||
lastTimeCheckedBonuses?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
||||||
|
|
|
@ -20,16 +20,17 @@ service cloud.firestore {
|
||||||
allow read;
|
allow read;
|
||||||
allow update: if resource.data.id == request.auth.uid
|
allow update: if resource.data.id == request.auth.uid
|
||||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']);
|
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId', 'lastPingTime']);
|
||||||
allow update: if resource.data.id == request.auth.uid
|
// User referral rules
|
||||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
allow update: if resource.data.id == request.auth.uid
|
||||||
.hasOnly(['referredByUserId'])
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
// only one referral allowed per user
|
.hasOnly(['referredByUserId'])
|
||||||
&& !("referredByUserId" in resource.data)
|
// only one referral allowed per user
|
||||||
// user can't refer themselves
|
&& !("referredByUserId" in resource.data)
|
||||||
&& !(resource.data.id == request.resource.data.referredByUserId);
|
// user can't refer themselves
|
||||||
// quid pro quos enabled (only once though so nbd) - bc I can't make this work:
|
&& !(resource.data.id == request.resource.data.referredByUserId);
|
||||||
// && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id);
|
// 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} {
|
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { FollowButton } from './follow-button'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { UserLink } from './user-page'
|
import { UserLink } from './user-page'
|
||||||
|
import { OnlineUserAvatar } from 'web/components/online-user-list'
|
||||||
|
|
||||||
export function FollowList(props: { userIds: string[] }) {
|
export function FollowList(props: { userIds: string[] }) {
|
||||||
const { userIds } = props
|
const { userIds } = props
|
||||||
|
@ -63,10 +64,7 @@ function UserFollowItem(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('items-center justify-between gap-2 p-2', className)}>
|
<Row className={clsx('items-center justify-between gap-2 p-2', className)}>
|
||||||
<Row className="items-center gap-2">
|
<OnlineUserAvatar user={user} />
|
||||||
<Avatar username={user?.username} avatarUrl={user?.avatarUrl} />
|
|
||||||
{user && <UserLink name={user.name} username={user.username} />}
|
|
||||||
</Row>
|
|
||||||
{!hideFollowButton && (
|
{!hideFollowButton && (
|
||||||
<FollowButton
|
<FollowButton
|
||||||
isFollowing={isFollowing}
|
isFollowing={isFollowing}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import clsx from 'clsx'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { usePrivateUser, useUser } from 'web/hooks/use-user'
|
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 { ManifoldLogo } from './manifold-logo'
|
||||||
import { MenuButton } from './menu'
|
import { MenuButton } from './menu'
|
||||||
import { ProfileSummary } from './profile-menu'
|
import { ProfileSummary } from './profile-menu'
|
||||||
|
@ -208,6 +208,17 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`,
|
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 (
|
return (
|
||||||
<nav aria-label="Sidebar" className={className}>
|
<nav aria-label="Sidebar" className={className}>
|
||||||
<ManifoldLogo className="py-6" twoLine />
|
<ManifoldLogo className="py-6" twoLine />
|
||||||
|
|
56
web/components/online-user-list.tsx
Normal file
56
web/components/online-user-list.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -55,6 +55,7 @@ import { FollowList } from 'web/components/follow-list'
|
||||||
import { SearchIcon } from '@heroicons/react/outline'
|
import { SearchIcon } from '@heroicons/react/outline'
|
||||||
import { useTipTxns } from 'web/hooks/use-tip-txns'
|
import { useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
|
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
|
||||||
|
import { OnlineUserList } from 'web/components/online-user-list'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
@ -174,7 +175,12 @@ export default function GroupPage(props: {
|
||||||
|
|
||||||
const rightSidebar = (
|
const rightSidebar = (
|
||||||
<Col className="mt-6 hidden xl:block">
|
<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>
|
</Col>
|
||||||
)
|
)
|
||||||
const leaderboard = (
|
const leaderboard = (
|
||||||
|
@ -254,7 +260,6 @@ export default function GroupPage(props: {
|
||||||
description={`Created by ${creator.name}. ${group.about}`}
|
description={`Created by ${creator.name}. ${group.about}`}
|
||||||
url={groupPath(group.slug)}
|
url={groupPath(group.slug)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Col className="px-3">
|
<Col className="px-3">
|
||||||
<Row className={'items-center justify-between gap-4'}>
|
<Row className={'items-center justify-between gap-4'}>
|
||||||
<div className={'sm:mb-1'}>
|
<div className={'sm:mb-1'}>
|
||||||
|
@ -270,7 +275,7 @@ export default function GroupPage(props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block xl:hidden">
|
<div className="hidden sm:block xl:hidden">
|
||||||
<JoinOrCreateButton
|
<JoinOrAddQuestionsButtons
|
||||||
group={group}
|
group={group}
|
||||||
user={user}
|
user={user}
|
||||||
isMember={!!isMember}
|
isMember={!!isMember}
|
||||||
|
@ -278,10 +283,13 @@ export default function GroupPage(props: {
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden">
|
||||||
<JoinOrCreateButton group={group} user={user} isMember={!!isMember} />
|
<JoinOrAddQuestionsButtons
|
||||||
|
group={group}
|
||||||
|
user={user}
|
||||||
|
isMember={!!isMember}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
currentPageForAnalytics={groupPath(group.slug)}
|
currentPageForAnalytics={groupPath(group.slug)}
|
||||||
className={'mb-0 sm:mb-2'}
|
className={'mb-0 sm:mb-2'}
|
||||||
|
@ -292,7 +300,7 @@ export default function GroupPage(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function JoinOrCreateButton(props: {
|
function JoinOrAddQuestionsButtons(props: {
|
||||||
group: Group
|
group: Group
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
isMember: boolean
|
isMember: boolean
|
||||||
|
|
Loading…
Reference in New Issue
Block a user