Skip to content

Commit

Permalink
Are You Being Followed? 👀 (#4785)
Browse files Browse the repository at this point in the history
* hover state to stop following button

* number of followers, no more avatar badge

* no canvas outline for being followed

* copy adjustment

* delete unnecessary

* nvm it was totally necessary

* just in case
  • Loading branch information
lankaukk authored Jan 23, 2024
1 parent 5a20c6f commit ecf81db
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 56 deletions.
124 changes: 91 additions & 33 deletions editor/src/components/canvas/multiplayer-presence.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import React from 'react'
import { Fragment } from 'react'
import { jsx } from '@emotion/react'
import type { User } from '@liveblocks/client'
import { AnimatePresence, motion } from 'framer-motion'
import { useAtom, useSetAtom } from 'jotai'
import React from 'react'
import type { Presence, PresenceActiveFrame, UserMeta } from '../../../liveblocks.config'
import {
useOthers,
Expand All @@ -15,6 +19,7 @@ import {
getCollaborator,
useAddMyselfToCollaborators,
useCanComment,
useMyUserAndPresence,
} from '../../core/commenting/comment-hooks'
import { MetadataUtils } from '../../core/model/element-metadata-utils'
import { mapDropNulls } from '../../core/shared/array-utils'
Expand All @@ -34,7 +39,7 @@ import {
useIsOnSameRemixRoute,
} from '../../core/shared/multiplayer'
import { assertNever } from '../../core/shared/utils'
import { Button, UtopiaStyles } from '../../uuiui'
import { Button, FlexRow, UtopiaStyles, colorTheme } from '../../uuiui'
import { notice } from '../common/notice'
import type { EditorAction } from '../editor/action-types'
import { isLoggedIn } from '../editor/action-types'
Expand Down Expand Up @@ -153,13 +158,13 @@ export const MultiplayerPresence = React.memo(() => {
}

return (
<>
<Fragment>
<FollowingOverlay />
<MultiplayerShadows />
{when(canComment, <CommentIndicators />)}
<MultiplayerCursors />
{when(canComment && isCommentMode(mode) && mode.comment != null, <CommentPopup />)}
</>
</Fragment>
)
})
MultiplayerPresence.displayName = 'MultiplayerPresence'
Expand Down Expand Up @@ -432,10 +437,31 @@ const FollowingOverlay = React.memo(() => {
return multiplayerColorFromIndex(followedUser?.colorIndex ?? null)
}, [followedUser])

const collabs = useStorage((store) => store.collaborators)
const connections = useStorage((store) => store.connections)

const { user: myUser, presence: myPresence } = useMyUserAndPresence()
const others = useOthers((list) =>
list
.filter((entry) => entry.connectionId !== myPresence.connectionId)
.map((other) => {
return {
...getCollaborator(collabs, other),
following: other.presence.following,
connectionId: other.connectionId,
connectedAt: connections?.[other.id]?.[other.connectionId]?.startedAt ?? 0,
}
}),
)

const myFollowers = others.filter((other) => other.following === myUser.id)
const followers = myFollowers.length
const hasFollowers = followers > 0

return (
<AnimatePresence>
{when(
followedUser != null,
followedUser != null || hasFollowers,
<motion.div
style={{
position: 'fixed',
Expand All @@ -445,44 +471,76 @@ const FollowingOverlay = React.memo(() => {
right: 0,
background: 'transparent',
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-end',
gap: 5,
paddingBottom: 14,
cursor: 'default',
border: `4px solid ${followedUserColor.background}`,
border: hasFollowers ? `undefined` : `4px solid ${followedUserColor.background}`,
}}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
>
<motion.div
style={{
backgroundColor: followedUserColor.background,
color: followedUserColor.foreground,
padding: '4px 4px 4px 12px',
borderRadius: 100,
boxShadow: UtopiaStyles.shadowStyles.mid.boxShadow,
display: 'flex',
alignItems: 'center',
gap: 10,
}}
>
<div>You're following {followedUser?.name}</div>
<Button
highlight
spotlight
onClick={stopFollowing}
{when(
followers > 0,
<motion.div
style={{
backgroundColor: colorTheme.primary.value,
color: colorTheme.white.value,
padding: '4px 12px',
borderRadius: 100,
boxShadow: UtopiaStyles.shadowStyles.mid.boxShadow,
display: 'flex',
alignItems: 'center',
}}
>
{followers === 1 ? (
<FlexRow style={{ height: 22, alignItems: 'center', justifyContent: 'center' }}>
1 person is following you
</FlexRow>
) : (
<FlexRow style={{ height: 22, alignItems: 'center', justifyContent: 'center' }}>
{followers} people are following you
</FlexRow>
)}
</motion.div>,
)}
{when(
followedUser != null,
<motion.div
style={{
backgroundColor: '#00000015',
padding: '4px 10px',
backgroundColor: followedUserColor.background,
color: followedUserColor.foreground,
padding: '4px 4px 4px 12px',
borderRadius: 100,
cursor: 'pointer',
boxShadow: UtopiaStyles.shadowStyles.mid.boxShadow,
display: 'flex',
alignItems: 'center',
gap: 5,
}}
>
Stop following
</Button>
</motion.div>
<div>You're following {followedUser?.name}</div>
<Button
highlight
spotlight
onClick={stopFollowing}
css={{
padding: '4px 10px',
borderRadius: 100,
cursor: 'pointer',
backgroundColor: '#00000025',
'&:hover': {
backgroundColor: '#00000015',
},
}}
>
Stop following
</Button>
</motion.div>,
)}
</motion.div>,
)}
</AnimatePresence>
Expand Down Expand Up @@ -564,7 +622,7 @@ const MultiplayerShadows = React.memo(() => {
)

return (
<>
<Fragment>
{shadows.map((shadow, index) => {
const { frame, action, source } = shadow.activeFrame
const color = multiplayerColorFromIndex(shadow.colorIndex)
Expand Down Expand Up @@ -625,7 +683,7 @@ const MultiplayerShadows = React.memo(() => {
</React.Fragment>
)
})}
</>
</Fragment>
)
})
MultiplayerShadows.displayName = 'MultiplayerShadows'
23 changes: 0 additions & 23 deletions editor/src/components/user-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -392,35 +392,12 @@ export const MultiplayerAvatar = React.memo((props: MultiplayerAvatarProps) => {
isOffline={props.isOffline}
/>
{props.isOwner ? <OwnerBadge isOffline={props.isOffline} /> : null}
{props.follower ? <FollowerBadge /> : null}
</div>
</Tooltip>
)
})
MultiplayerAvatar.displayName = 'MultiplayerAvatar'

const FollowerBadge = React.memo(() => {
return (
<div
style={{
position: 'absolute',
top: -1,
left: -1,
borderRadius: '100%',
backgroundColor: colorTheme.primary.value,
color: colorTheme.white.value,
width: 8,
height: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: `1px solid ${colorTheme.bg1.value}`,
}}
/>
)
})
FollowerBadge.displayName = 'FollowerBadge'

interface OwnerBadge {
isOffline?: boolean
}
Expand Down

0 comments on commit ecf81db

Please sign in to comment.