Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Add Feature Flag System #10099

Merged
merged 16 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ import {
} from '@etherealengine/client-core/src/common/services/MediaInstanceConnectionService'
import { ChannelService, ChannelState } from '@etherealengine/client-core/src/social/services/ChannelService'
import { LocationState } from '@etherealengine/client-core/src/social/services/LocationService'
import { InstanceID, LocationID, RoomCode } from '@etherealengine/common/src/schema.type.module'
import { FeatureFlag, InstanceID, LocationID, RoomCode } from '@etherealengine/common/src/schema.type.module'
import { getMutableState, getState, none, useHookstate, useMutableState } from '@etherealengine/hyperflux'
import { NetworkState } from '@etherealengine/network'

import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { FriendService } from '../social/services/FriendService'
import { connectToInstance } from '../transports/SocketWebRTCClientFunctions'
import { PopupMenuState } from '../user/components/UserMenu/PopupMenuService'
import FriendsMenu from '../user/components/UserMenu/menus/FriendsMenu'
import MessagesMenu from '../user/components/UserMenu/menus/MessagesMenu'
import { PopupMenuState } from '../user/components/UserMenu/PopupMenuService'

export const WorldInstanceProvisioning = () => {
const locationState = useMutableState(LocationState)
Expand Down Expand Up @@ -243,33 +244,45 @@ export const SocialMenus = {
Messages: 'Messages'
}

const SocialMenuFlag = 'ir.client.menu.social' as FeatureFlag

export const FriendMenus = () => {
const { t } = useTranslation()
FriendService.useAPIListeners()

const socialsEnabled = FeatureFlagsState.useEnabled(SocialMenuFlag)

useEffect(() => {
const menuState = getMutableState(PopupMenuState)
menuState.menus.merge({
if (!socialsEnabled) return

const popupMenuState = getMutableState(PopupMenuState)
popupMenuState.menus.merge({
[SocialMenus.Friends]: FriendsMenu,
[SocialMenus.Messages]: MessagesMenu
})
menuState.hotbar.merge({

popupMenuState.hotbar.merge({
[SocialMenus.Friends]: { icon: <Groups />, tooltip: t('user:menu.friends') }
})

return () => {
menuState.menus.merge({
popupMenuState.menus.merge({
[SocialMenus.Friends]: none,
[SocialMenus.Messages]: none
})

menuState.hotbar.merge({
popupMenuState.hotbar.merge({
[SocialMenus.Friends]: none
})
}
}, [])
}, [socialsEnabled])

return null
if (!socialsEnabled) return null

const UseFriendsListeners = () => {
FriendService.useAPIListeners()
return null
}
return <UseFriendsListeners />
}

export const InstanceProvisioning = () => {
Expand Down
83 changes: 70 additions & 13 deletions packages/client-core/src/user/UserUISystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions'
import { PresentationSystemGroup } from '@etherealengine/ecs/src/SystemGroups'
import { getMutableState, none } from '@etherealengine/hyperflux'

import { FeatureFlag } from '@etherealengine/common/src/schema.type.module'
import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { InviteService } from '../social/services/InviteService'
import { PopupMenuState } from './components/UserMenu/PopupMenuService'
import AvatarCreatorMenu, { SupportedSdks } from './components/UserMenu/menus/AvatarCreatorMenu'
import AvatarModifyMenu from './components/UserMenu/menus/AvatarModifyMenu'
import AvatarSelectMenu from './components/UserMenu/menus/AvatarSelectMenu'
import EmoteMenu from './components/UserMenu/menus/EmoteMenu'
import ProfileMenu from './components/UserMenu/menus/ProfileMenu'
import SettingMenu from './components/UserMenu/menus/SettingMenu'
import ShareMenu from './components/UserMenu/menus/ShareMenu'
import { PopupMenuState } from './components/UserMenu/PopupMenuService'

export const EmoteIcon = () => (
<svg width="35px" height="35px" viewBox="0 0 184 184" version="1.1">
Expand All @@ -64,10 +66,18 @@ export const UserMenus = {
Emote: 'user.Emote'
}

export const EmoteMenuFlag = 'ir.client.menu.emote' as FeatureFlag
export const AvaturnMenuFlag = 'ir.client.menu.avaturn' as FeatureFlag
export const RPMMenuFlag = 'ir.client.menu.readyPlayerMe' as FeatureFlag

const reactor = () => {
const { t } = useTranslation()
InviteService.useAPIListeners()

const emotesEnabled = FeatureFlagsState.useEnabled(EmoteMenuFlag)
const avaturnEnabled = FeatureFlagsState.useEnabled(AvaturnMenuFlag)
const rpmEnabled = FeatureFlagsState.useEnabled(RPMMenuFlag)

useEffect(() => {
const FaceRetouchingNatural = lazy(() => import('@mui/icons-material/FaceRetouchingNatural'))
const Send = lazy(() => import('@mui/icons-material/Send'))
Expand All @@ -78,16 +88,12 @@ const reactor = () => {
[UserMenus.Settings]: SettingMenu,
[UserMenus.AvatarSelect]: AvatarSelectMenu,
[UserMenus.AvatarModify]: AvatarModifyMenu,
[UserMenus.ReadyPlayer]: AvatarCreatorMenu(SupportedSdks.ReadyPlayerMe),
[UserMenus.Avaturn]: AvatarCreatorMenu(SupportedSdks.Avaturn),
[UserMenus.Share]: ShareMenu,
[UserMenus.Emote]: EmoteMenu
[UserMenus.Share]: ShareMenu
})

popupMenuState.hotbar.merge({
[UserMenus.Profile]: { icon: <FaceRetouchingNatural />, tooltip: t('user:menu.settings') },
[UserMenus.Share]: { icon: <Send />, tooltip: t('user:menu.sendLocation') },
[UserMenus.Emote]: { icon: <EmoteIcon />, tooltip: t('user:menu.emote') }
[UserMenus.Share]: { icon: <Send />, tooltip: t('user:menu.sendLocation') }
})

return () => {
Expand All @@ -96,19 +102,70 @@ const reactor = () => {
[UserMenus.Settings]: none,
[UserMenus.AvatarSelect]: none,
[UserMenus.AvatarModify]: none,
[UserMenus.ReadyPlayer]: none,
[UserMenus.Avaturn]: none,
[UserMenus.Share]: none,
[UserMenus.Emote]: none
[UserMenus.Share]: none
})

popupMenuState.hotbar.merge({
[UserMenus.Profile]: none,
[UserMenus.Share]: none,
[UserMenus.Emote]: none
[UserMenus.Share]: none
})
}
}, [])

useEffect(() => {
if (!emotesEnabled) return

const popupMenuState = getMutableState(PopupMenuState)

popupMenuState.menus.merge({
[UserMenus.Emote]: EmoteMenu
})

popupMenuState.hotbar.merge({
[UserMenus.Emote]: { icon: <EmoteIcon />, tooltip: t('user:menu.emote') }
})

return () => {
popupMenuState.menus.merge({
[UserMenus.Emote]: none
})

popupMenuState.hotbar.merge({
[UserMenus.Emote]: none
})
}
}, [emotesEnabled])

useEffect(() => {
if (!avaturnEnabled) return

const popupMenuState = getMutableState(PopupMenuState)

popupMenuState.menus.merge({
[UserMenus.ReadyPlayer]: AvatarCreatorMenu(SupportedSdks.ReadyPlayerMe)
})
return () => {
popupMenuState.menus.merge({
[UserMenus.ReadyPlayer]: none
})
}
}, [avaturnEnabled])

useEffect(() => {
if (!rpmEnabled) return

const popupMenuState = getMutableState(PopupMenuState)

popupMenuState.menus.merge({
[UserMenus.Avaturn]: AvatarCreatorMenu(SupportedSdks.Avaturn)
})
return () => {
popupMenuState.menus.merge({
[UserMenus.Avaturn]: none
})
}
}, [rpmEnabled])

return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ import Grid from '@etherealengine/ui/src/primitives/mui/Grid'
import Icon from '@etherealengine/ui/src/primitives/mui/Icon'
import IconButton from '@etherealengine/ui/src/primitives/mui/IconButton'

import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { AvaturnMenuFlag, RPMMenuFlag, UserMenus } from '../../../UserUISystem'
import { AvatarService } from '../../../services/AvatarService'
import { UserMenus } from '../../../UserUISystem'
import styles from '../index.module.scss'
import { PopupMenuServices } from '../PopupMenuService'
import styles from '../index.module.scss'

interface Props {
selectedAvatar?: AvatarType
Expand Down Expand Up @@ -83,6 +84,8 @@ const AvatarModifyMenu = ({ selectedAvatar }: Props) => {
const [isSaving, setIsSaving] = useState(false)
const avatarRef = useRef<HTMLInputElement | null>(null)
const thumbnailRef = useRef<HTMLInputElement | null>(null)
const avaturnEnabled = FeatureFlagsState.useEnabled(AvaturnMenuFlag)
const rpmEnabled = FeatureFlagsState.useEnabled(RPMMenuFlag)

let thumbnailSrc = state.thumbnailUrl
if (state.thumbnailFile) {
Expand Down Expand Up @@ -346,23 +349,27 @@ const AvatarModifyMenu = ({ selectedAvatar }: Props) => {
</Grid>

<Grid item md={5} sx={{ width: '100%' }}>
<Button
fullWidth
type="gradientRounded"
sx={{ mt: 1 }}
onClick={() => PopupMenuServices.showPopupMenu(UserMenus.ReadyPlayer)}
>
{t('user:usermenu.profile.useReadyPlayerMe')}
</Button>
{rpmEnabled && (
<Button
fullWidth
type="gradientRounded"
sx={{ mt: 1 }}
onClick={() => PopupMenuServices.showPopupMenu(UserMenus.ReadyPlayer)}
>
{t('user:usermenu.profile.useReadyPlayerMe')}
</Button>
)}

<Button
fullWidth
type="gradientRounded"
sx={{ mt: 1 }}
onClick={() => PopupMenuServices.showPopupMenu(UserMenus.Avaturn)}
>
{t('user:usermenu.profile.useAvaturn')}
</Button>
{avaturnEnabled && (
<Button
fullWidth
type="gradientRounded"
sx={{ mt: 1 }}
onClick={() => PopupMenuServices.showPopupMenu(UserMenus.Avaturn)}
>
{t('user:usermenu.profile.useAvaturn')}
</Button>
)}

<InputText
name="name"
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/schema.type.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type * from './schemas/setting/chargebee-setting.schema'
export type * from './schemas/setting/client-setting.schema'
export type * from './schemas/setting/coil-setting.schema'
export type * from './schemas/setting/email-setting.schema'
export type * from './schemas/setting/feature-flag-setting.schema'
export type * from './schemas/setting/helm-setting.schema'
export type * from './schemas/setting/instance-server-setting.schema'
export type * from './schemas/setting/redis-setting.schema'
Expand Down Expand Up @@ -224,6 +225,8 @@ export const taskServerSettingPath = 'task-server-setting'

export const emailSettingPath = 'email-setting'

export const featureFlagSettingPath = 'feature-flag-setting'

export const instanceServerSettingPath = 'instance-server-setting'

export const clientSettingPath = 'client-setting'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
CPAL-1.0 License

The contents of this file are subject to the Common Public Attribution License
Version 1.0. (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE.
The License is based on the Mozilla Public License Version 1.1, but Sections 14
and 15 have been added to cover use of software over a computer network and
provide for limited attribution for the Original Developer. In addition,
Exhibit A has been modified to be consistent with Exhibit B.

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
specific language governing rights and limitations under the License.

The Original Code is Ethereal Engine.

The Original Developer is the Initial Developer. The Initial Developer of the
Original Code is the Ethereal Engine team.

All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023
Ethereal Engine. All Rights Reserved.
*/

// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
import type { Static } from '@feathersjs/typebox'
import { getValidator, querySyntax, Type } from '@feathersjs/typebox'
import { OpaqueType } from '../../interfaces/OpaqueType'
import { TypedString } from '../../types/TypeboxUtils'
import { dataValidator, queryValidator } from '../validators'

export type FeatureFlag = OpaqueType<'FeatureFlag'> & string

export const featureFlagSettingPath = 'feature-flag-setting'

export const featureFlagSettingMethods = ['find', 'get', 'create', 'patch', 'remove'] as const

// Main data model schema
export const featureFlagSettingSchema = Type.Object(
{
id: Type.String({
format: 'uuid'
}),
flagName: TypedString<FeatureFlag>(),
flagValue: Type.Boolean(),
createdAt: Type.String({ format: 'date-time' }),
updatedAt: Type.String({ format: 'date-time' })
},
{ $id: 'FeatureFlagSetting', additionalProperties: false }
)
export interface FeatureFlagSettingType extends Static<typeof featureFlagSettingSchema> {}

// Schema for creating new entries
export const featureFlagSettingDataSchema = Type.Pick(featureFlagSettingSchema, ['flagName', 'flagValue'], {
$id: 'FeatureFlagSettingData'
})
export interface FeatureFlagSettingData extends Static<typeof featureFlagSettingDataSchema> {}

// Schema for updating existing entries
export const featureFlagSettingPatchSchema = Type.Partial(featureFlagSettingSchema, {
$id: 'FeatureFlagSettingPatch'
})
export interface FeatureFlagSettingPatch extends Static<typeof featureFlagSettingPatchSchema> {}

// Schema for allowed query properties
export const featureFlagSettingQueryProperties = Type.Pick(featureFlagSettingSchema, ['id', 'flagName', 'flagValue'])
export const featureFlagSettingQuerySchema = Type.Intersect(
[
querySyntax(featureFlagSettingQueryProperties),
// Add additional query properties here
Type.Object({}, { additionalProperties: false })
],
{ additionalProperties: false }
)
export interface FeatureFlagSettingQuery extends Static<typeof featureFlagSettingQuerySchema> {}

export const featureFlagSettingValidator = /* @__PURE__ */ getValidator(featureFlagSettingSchema, dataValidator)
export const featureFlagSettingDataValidator = /* @__PURE__ */ getValidator(featureFlagSettingDataSchema, dataValidator)
export const featureFlagSettingPatchValidator = /* @__PURE__ */ getValidator(
featureFlagSettingPatchSchema,
dataValidator
)
export const featureFlagSettingQueryValidator = /* @__PURE__ */ getValidator(
featureFlagSettingQuerySchema,
queryValidator
)
1 change: 1 addition & 0 deletions packages/engine/src/EngineModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Ethereal Engine. All Rights Reserved.
/** World Module */
import '@etherealengine/spatial'

export * from './FeatureFlagsState'
export * from './avatar/AvatarModule'
export * from './interaction/InteractionModule'
export * from './interaction/MediaModule'
Expand Down
Loading
Loading