feat: toggleable feature flags (#209)
parent
f08777f629
commit
69d009d02a
|
@ -18,6 +18,7 @@ const buildTimeAgo = useTimeAgo(buildTime)
|
||||||
/>
|
/>
|
||||||
</CommonTooltip>
|
</CommonTooltip>
|
||||||
<NavSelectLanguage />
|
<NavSelectLanguage />
|
||||||
|
<NavSelectFeatureFlags v-if="currentUser" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button cursor-pointer hover:underline @click="openPreviewHelp">
|
<button cursor-pointer hover:underline @click="openPreviewHelp">
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const featureFlags = useFeatureFlags()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CommonTooltip :content="t('nav_footer.select_feature_flags')">
|
||||||
|
<CommonDropdown>
|
||||||
|
<button flex>
|
||||||
|
<div i-ri:flag-line text-lg />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<CommonDropdownItem
|
||||||
|
:checked="featureFlags.experimentalVirtualScroll"
|
||||||
|
@click="toggleFeatureFlag('experimentalVirtualScroll')"
|
||||||
|
>
|
||||||
|
{{ t('feature_flag.virtual_scroll') }}
|
||||||
|
</CommonDropdownItem>
|
||||||
|
</template>
|
||||||
|
</CommonDropdown>
|
||||||
|
</CommonTooltip>
|
||||||
|
</template>
|
|
@ -10,7 +10,7 @@ const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonTooltip placement="bottom" :content="t('nav_footer.select_language')">
|
<CommonTooltip :content="t('nav_footer.select_language')">
|
||||||
<CommonDropdown>
|
<CommonDropdown>
|
||||||
<button flex>
|
<button flex>
|
||||||
<div i-ri:earth-line text-lg />
|
<div i-ri:earth-line text-lg />
|
||||||
|
|
|
@ -7,23 +7,27 @@ const { paginator, stream } = defineProps<{
|
||||||
paginator: Paginator<any, Status[]>
|
paginator: Paginator<any, Status[]>
|
||||||
stream?: WsEvents
|
stream?: WsEvents
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const virtualScroller = $(computedEager(() => useFeatureFlags().experimentalVirtualScroll))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonPaginator v-bind="{ paginator, stream }" virtual-scroller>
|
<CommonPaginator v-bind="{ paginator, stream }" :virtual-scroller="virtualScroller">
|
||||||
<template #updater="{ number, update }">
|
<template #updater="{ number, update }">
|
||||||
<button py-4 border="b base" flex="~ col" p-3 w-full text-primary font-bold @click="update">
|
<button py-4 border="b base" flex="~ col" p-3 w-full text-primary font-bold @click="update">
|
||||||
Show {{ number }} new items
|
Show {{ number }} new items
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ item, active }">
|
<template #default="{ item, active }">
|
||||||
|
<template v-if="virtualScroller">
|
||||||
<DynamicScrollerItem :item="item" :active="active" tag="article">
|
<DynamicScrollerItem :item="item" :active="active" tag="article">
|
||||||
<StatusCard
|
<StatusCard :status="item" border="b base" py-3 />
|
||||||
:status="item"
|
|
||||||
border="b base" py-3
|
|
||||||
/>
|
|
||||||
</DynamicScrollerItem>
|
</DynamicScrollerItem>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<StatusCard :status="item" border="b base" py-3 />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
<template #loading>
|
<template #loading>
|
||||||
<StatusCardSkeleton border="b base" py-3 />
|
<StatusCardSkeleton border="b base" py-3 />
|
||||||
<StatusCardSkeleton border="b base" py-3 op50 />
|
<StatusCardSkeleton border="b base" py-3 op50 />
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import type { Account } from 'masto'
|
||||||
|
import { STORAGE_KEY_FEATURE_FLAGS } from '~/constants'
|
||||||
|
|
||||||
|
export interface FeatureFlags {
|
||||||
|
experimentalVirtualScroll: boolean
|
||||||
|
}
|
||||||
|
export type FeatureFlagsMap = Record<string, FeatureFlags>
|
||||||
|
|
||||||
|
export const allFeatureFlags = useLocalStorage<FeatureFlagsMap>(STORAGE_KEY_FEATURE_FLAGS, {}, { deep: true })
|
||||||
|
|
||||||
|
export function getDefaultFeatureFlags(): FeatureFlags {
|
||||||
|
return {
|
||||||
|
experimentalVirtualScroll: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const currentUserFeatureFlags = computed(() => {
|
||||||
|
if (!currentUser.value?.account.id)
|
||||||
|
return {} as FeatureFlags
|
||||||
|
|
||||||
|
const id = `${currentUser.value.account.acct}@${currentUser.value.server}`
|
||||||
|
|
||||||
|
if (!allFeatureFlags.value[id])
|
||||||
|
allFeatureFlags.value[id] = getDefaultFeatureFlags()
|
||||||
|
|
||||||
|
return allFeatureFlags.value[id] as FeatureFlags
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useFeatureFlags() {
|
||||||
|
const featureFlags = currentUserFeatureFlags.value
|
||||||
|
|
||||||
|
return featureFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleFeatureFlag(key: keyof FeatureFlags) {
|
||||||
|
const featureFlags = currentUserFeatureFlags.value
|
||||||
|
|
||||||
|
if (featureFlags[key])
|
||||||
|
featureFlags[key] = !featureFlags[key]
|
||||||
|
else
|
||||||
|
featureFlags[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearUserFeatureFlags(account?: Account) {
|
||||||
|
if (!account)
|
||||||
|
account = currentUser.value?.account
|
||||||
|
|
||||||
|
if (!account)
|
||||||
|
return
|
||||||
|
|
||||||
|
const id = `${account.acct}@${currentUser.value?.server}`
|
||||||
|
if (!allFeatureFlags.value[id])
|
||||||
|
return
|
||||||
|
|
||||||
|
delete allFeatureFlags.value[id]
|
||||||
|
}
|
||||||
|
|
|
@ -73,6 +73,7 @@ export async function signout() {
|
||||||
// Clear stale data
|
// Clear stale data
|
||||||
delete servers.value[_currentUserId]
|
delete servers.value[_currentUserId]
|
||||||
clearUserDrafts()
|
clearUserDrafts()
|
||||||
|
clearUserFeatureFlags()
|
||||||
|
|
||||||
// Remove the current user from the users
|
// Remove the current user from the users
|
||||||
users.value.splice(index, 1)
|
users.value.splice(index, 1)
|
||||||
|
|
|
@ -11,4 +11,4 @@ export const STORAGE_KEY_NOTIFY_TAB = 'elk-notify-tab'
|
||||||
export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit'
|
export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit'
|
||||||
export const STORAGE_KEY_ZEN_MODE = 'elk-zenmode'
|
export const STORAGE_KEY_ZEN_MODE = 'elk-zenmode'
|
||||||
export const STORAGE_KEY_LANG = 'elk-lang'
|
export const STORAGE_KEY_LANG = 'elk-lang'
|
||||||
|
export const STORAGE_KEY_FEATURE_FLAGS = 'elk-feature-flags'
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
"profile": "Profil"
|
"profile": "Profil"
|
||||||
},
|
},
|
||||||
"nav_footer": {
|
"nav_footer": {
|
||||||
"select_language": "Sprache auswählen"
|
"select_language": "Sprache auswählen",
|
||||||
|
"select_feature_flags": "Feature-Flags umschalten"
|
||||||
},
|
},
|
||||||
"timeline": "Timeline",
|
"timeline": "Timeline",
|
||||||
"title": {
|
"title": {
|
||||||
|
@ -22,5 +23,8 @@
|
||||||
"posts": "{0} Beiträge",
|
"posts": "{0} Beiträge",
|
||||||
"following": "{0} Folge ich",
|
"following": "{0} Folge ich",
|
||||||
"followers": "{0} Follower"
|
"followers": "{0} Follower"
|
||||||
|
},
|
||||||
|
"feature_flag": {
|
||||||
|
"virtual_scroll": "Virtuelles Scrollen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
"profile": "Profile"
|
"profile": "Profile"
|
||||||
},
|
},
|
||||||
"nav_footer": {
|
"nav_footer": {
|
||||||
"select_language": "Select Language"
|
"select_language": "Select Language",
|
||||||
|
"select_feature_flags": "Toggle Feature Flags"
|
||||||
},
|
},
|
||||||
"timeline": "Timeline",
|
"timeline": "Timeline",
|
||||||
"title": {
|
"title": {
|
||||||
|
@ -22,5 +23,8 @@
|
||||||
"posts": "{0} Posts",
|
"posts": "{0} Posts",
|
||||||
"following": "{0} Following",
|
"following": "{0} Following",
|
||||||
"followers": "{0} Followers"
|
"followers": "{0} Followers"
|
||||||
|
},
|
||||||
|
"feature_flag": {
|
||||||
|
"virtual_scroll": "Virtual Scrolling"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
"profile": "个人资料"
|
"profile": "个人资料"
|
||||||
},
|
},
|
||||||
"nav_footer": {
|
"nav_footer": {
|
||||||
"select_language": "选择语言"
|
"select_language": "选择语言",
|
||||||
|
"select_feature_flags": "切换功能标志"
|
||||||
},
|
},
|
||||||
"timeline": "时间轴",
|
"timeline": "时间轴",
|
||||||
"title": {
|
"title": {
|
||||||
|
@ -22,5 +23,8 @@
|
||||||
"followers": "被 {0} 人关注",
|
"followers": "被 {0} 人关注",
|
||||||
"following": "正在关注 {0} 人",
|
"following": "正在关注 {0} 人",
|
||||||
"posts": "{0} 条帖文"
|
"posts": "{0} 条帖文"
|
||||||
|
},
|
||||||
|
"feature_flag": {
|
||||||
|
"virtual_scroll": "虚拟滚动"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue