Skip to content

Commit

Permalink
[base] Improve user color tools
Browse files Browse the repository at this point in the history
  • Loading branch information
mariuslundgard authored and rexxars committed Oct 6, 2020
1 parent f7d989b commit 81f4247
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 51 deletions.
35 changes: 10 additions & 25 deletions packages/@sanity/base/src/components/SanityRoot.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useRef, useEffect} from 'react'
import React, {useRef, useState} from 'react'
import config from 'config:sanity'
import RootComponent from 'part:@sanity/base/root'
import {PortalProvider} from 'part:@sanity/components/portal'
Expand All @@ -13,41 +13,26 @@ import DevServerStatus from './DevServerStatus'
function SanityRoot() {
const {projectId, dataset} = config.api || {}
const rootRef = useRef(null)
const portalRef = useRef(document.createElement('div'))

// attach the global portal element
useEffect(() => {
// set a data attribute for debugging
portalRef.current.setAttribute('data-portal', '')

if (rootRef.current) {
rootRef.current.appendChild(portalRef.current)
}

return () => {
if (rootRef.current) {
rootRef.current.removeChild(portalRef.current)
}
}
}, [])
const [portalElement, setPortalElement] = useState(() => document.createElement('div'))

if (!projectId || !dataset) {
return <MissingProjectConfig />
}

return (
<div className={styles.root} ref={rootRef}>
<PortalProvider element={portalRef.current}>
<UserColorManagerProvider manager={userColorManager}>
<SnackbarProvider>
<UserColorManagerProvider manager={userColorManager}>
<PortalProvider element={portalElement}>
<SnackbarProvider>
<div className={styles.root} ref={rootRef}>
<DevServerStatus />
<ErrorHandler />
<RootComponent />
<VersionChecker />
</SnackbarProvider>
</UserColorManagerProvider>
</div>
<div data-portal="" ref={setPortalElement} />
</SnackbarProvider>
</PortalProvider>
</div>
</UserColorManagerProvider>
)
}

Expand Down
7 changes: 5 additions & 2 deletions packages/@sanity/base/src/components/UserAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,17 @@ export function UserAvatar(props: Props) {

function StaticUserAvatar({user, animateArrowFrom, position, size, status, tone}: LoadedUserProps) {
const [imageLoadError, setImageLoadError] = useState<null | Error>(null)
const userColor = useUserColor(user.id) || {border: ''}
const userColor = useUserColor(user.id)
const imageUrl = imageLoadError ? undefined : user?.imageUrl

return (
<Avatar
animateArrowFrom={animateArrowFrom}
arrowPosition={position}
color={userColor.border}
color={{
dark: userColor.tints[400].hex,
light: userColor.tints[500].hex
}}
initials={user?.displayName && nameToInitials(user.displayName)}
src={imageUrl}
onImageLoadError={setImageLoadError}
Expand Down
5 changes: 3 additions & 2 deletions packages/@sanity/base/src/user-color/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export function useUserColorManager(): UserColorManager {
return useContext(UserColorManagerContext)
}

export function useUserColor(userId: string | null): UserColor | null {
export function useUserColor(userId: string | null): UserColor {
const manager = useUserColorManager()
return useObservable(userId === null ? of(null) : manager.listen(userId))

return useObservable(userId === null ? of(manager.get(null)) : manager.listen(userId))
}
49 changes: 31 additions & 18 deletions packages/@sanity/base/src/user-color/manager.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {Observable} from 'rxjs'
import {filter, shareReplay} from 'rxjs/operators'
import {color as SanityColor, ColorHueKey, COLOR_HUES} from '@sanity/color'
import {UserColorHue, UserColorManager, UserColor} from './types'

type UserId = string
import {UserColorHue, UserColorManager, UserColor, UserId} from './types'

export interface UserColorManagerOptions {
anonymousColor?: UserColor
userStore?: {currentUser: Observable<{type: 'snapshot' | 'error'; user?: {id: string} | null}>}
colors?: Readonly<Record<UserColorHue, UserColor>>
currentUserColor?: UserColorHue
Expand All @@ -18,23 +17,33 @@ const defaultCurrentUserHue: ColorHueKey = 'purple'
const defaultHues: ColorHueKey[] = COLOR_HUES.filter(
hue => hue !== 'green' && hue !== 'red' && hue !== 'gray'
)

const defaultColors = defaultHues.reduce((colors, hue) => {
colors[hue] = {
background: SanityColor[hue][100].hex,
border: SanityColor[hue][300].hex,
text: SanityColor[hue][700].hex
text: SanityColor[hue][700].hex,
tints: SanityColor[hue]
}
return colors
}, {} as Record<ColorHueKey, UserColor>)

const defaultAnonymousColor: UserColor = {
background: SanityColor.gray[100].hex,
border: SanityColor.gray[300].hex,
text: SanityColor.gray[700].hex,
tints: SanityColor.gray
}

export function createUserColorManager(options?: UserColorManagerOptions): UserColorManager {
const colors = (options && options.colors) || defaultColors
const userColors = (options && options.colors) || defaultColors
const anonymousColor = options?.anonymousColor || defaultAnonymousColor
const currentUserColor = (options && options.currentUserColor) || defaultCurrentUserHue
if (!colors.hasOwnProperty(currentUserColor)) {
if (!userColors.hasOwnProperty(currentUserColor)) {
throw new Error(`'colors' must contain 'currentUserColor' (${currentUserColor})`)
}

const colorHues: UserColorHue[] = Object.keys(colors)
const colorHues: UserColorHue[] = Object.keys(userColors)
const subscriptions = new Map<UserId, Observable<UserColor>>()
const previouslyAssigned = new Map<UserId, UserColorHue>()
const assignedCounts: Record<UserColorHue, number> = colorHues.reduce((counts, color) => {
Expand All @@ -46,7 +55,7 @@ export function createUserColorManager(options?: UserColorManagerOptions): UserC
// but is useful for debugging and poses a minimal overhead
const assigned = new Map<UserId, UserColorHue>()

let currentUserId: string | null
let currentUserId: UserId | null

if (options?.userStore) {
options.userStore.currentUser
Expand All @@ -56,11 +65,15 @@ export function createUserColorManager(options?: UserColorManagerOptions): UserC

return {get, listen}

function get(userId: string): UserColor {
return colors[getUserHue(userId)]
function get(userId: UserId | null): UserColor {
if (!userId) {
return anonymousColor
}

return userColors[getUserHue(userId)]
}

function getUserHue(userId: string): UserColorHue {
function getUserHue(userId: UserId): UserColorHue {
if (userId === currentUserId) {
return currentUserColor
}
Expand Down Expand Up @@ -113,7 +126,7 @@ export function createUserColorManager(options?: UserColorManagerOptions): UserC
}

function getUnusedColor(): UserColorHue | undefined {
return colorHues.find(color => assignedCounts[color] === 0)
return colorHues.find(colorHue => assignedCounts[colorHue] === 0)
}

function hasUnusedColor(): boolean {
Expand All @@ -124,13 +137,13 @@ export function createUserColorManager(options?: UserColorManagerOptions): UserC
let leastUses = +Infinity
let leastUsed: UserColorHue[] = []

colorHues.forEach(color => {
const uses = assignedCounts[color]
colorHues.forEach(colorHue => {
const uses = assignedCounts[colorHue]
if (uses === leastUses) {
leastUsed.push(color)
leastUsed.push(colorHue)
} else if (uses < leastUses) {
leastUses = uses
leastUsed = [color]
leastUsed = [colorHue]
}
})

Expand All @@ -141,8 +154,8 @@ export function createUserColorManager(options?: UserColorManagerOptions): UserC

function getObservableColor(userId: string, hue: UserColorHue): Observable<UserColor> {
return new Observable<UserColor>(subscriber => {
const color = colors[hue]
subscriber.next(color)
const userColor = userColors[hue]
subscriber.next(userColor)
return () => {
subscriptions.delete(userId)
unassignHue(userId, hue)
Expand Down
11 changes: 7 additions & 4 deletions packages/@sanity/base/src/user-color/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import {ColorTints} from '@sanity/color'
import {Observable} from 'rxjs'

// For better readability
export type UserColorHue = string
export type HexColor = string
export type UserColorHue = string
export type UserId = string

export type UserColor = Readonly<{
background: HexColor
text: HexColor
border: HexColor
text: HexColor
tints: ColorTints
}>

export interface UserColorManager {
get: (userId: string) => UserColor
listen: (userId: string) => Observable<UserColor>
get: (userId: UserId | null) => UserColor
listen: (userId: UserId) => Observable<UserColor>
}

0 comments on commit 81f4247

Please sign in to comment.