Skip to content

Commit 2cb070c

Browse files
authored
feat: allow choosing favorite buttons in bottom navigation bar (elk-zone#2761)
1 parent 2a6a994 commit 2cb070c

File tree

14 files changed

+286
-47
lines changed

14 files changed

+286
-47
lines changed

components/nav/NavBottom.vue

Lines changed: 32 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,45 @@
11
<script setup lang="ts">
2-
// only one icon can be lit up at the same time
3-
import { STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
2+
import type { Component } from 'vue'
3+
import type { NavButtonName } from '../../composables/settings'
44
5-
const moreMenuVisible = ref(false)
5+
import { STORAGE_KEY_BOTTOM_NAV_BUTTONS } from '~/constants'
6+
7+
import { NavButtonExplore, NavButtonFederated, NavButtonHome, NavButtonLocal, NavButtonMention, NavButtonMoreMenu, NavButtonNotification, NavButtonSearch } from '#components'
8+
9+
interface NavButton {
10+
name: string
11+
component: Component
12+
}
13+
14+
const navButtons: NavButton[] = [
15+
{ name: 'home', component: NavButtonHome },
16+
{ name: 'search', component: NavButtonSearch },
17+
{ name: 'notification', component: NavButtonNotification },
18+
{ name: 'mention', component: NavButtonMention },
19+
{ name: 'explore', component: NavButtonExplore },
20+
{ name: 'local', component: NavButtonLocal },
21+
{ name: 'federated', component: NavButtonFederated },
22+
{ name: 'moreMenu', component: NavButtonMoreMenu },
23+
]
624
7-
const { notifications } = useNotifications()
8-
const lastAccessedNotificationRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE, '')
9-
const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, '')
25+
const defaultSelectedNavButtonNames: NavButtonName[] = currentUser.value
26+
? ['home', 'search', 'notification', 'mention', 'moreMenu']
27+
: ['explore', 'local', 'federated', 'moreMenu']
28+
const selectedNavButtonNames = useLocalStorage<NavButtonName[]>(STORAGE_KEY_BOTTOM_NAV_BUTTONS, defaultSelectedNavButtonNames)
29+
30+
const selectedNavButtons = computed(() => selectedNavButtonNames.value.map(name => navButtons.find(navButton => navButton.name === name)))
31+
32+
// only one icon can be lit up at the same time
33+
const moreMenuVisible = ref(false)
1034
</script>
1135

1236
<template>
37+
<!-- This weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. -->
1338
<nav
1439
h-14 border="t base" flex flex-row text-xl
1540
of-y-scroll scrollbar-hide overscroll-none
1641
class="after-content-empty after:(h-[calc(100%+0.5px)] w-0.1px pointer-events-none)"
1742
>
18-
<!-- These weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. -->
19-
<template v-if="currentUser">
20-
<NuxtLink to="/home" :aria-label="$t('nav.home')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
21-
<div i-ri:home-5-line />
22-
</NuxtLink>
23-
<NuxtLink to="/search" :aria-label="$t('nav.search')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
24-
<div i-ri:search-line />
25-
</NuxtLink>
26-
<NuxtLink :to="`/notifications/${lastAccessedNotificationRoute}`" :aria-label="$t('nav.notifications')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
27-
<div flex relative>
28-
<div class="i-ri:notification-4-line" text-xl />
29-
<div v-if="notifications" class="top-[-0.3rem] right-[-0.3rem]" absolute font-bold rounded-full h-4 w-4 text-xs bg-primary text-inverted flex items-center justify-center>
30-
{{ notifications < 10 ? notifications : '•' }}
31-
</div>
32-
</div>
33-
</NuxtLink>
34-
<NuxtLink to="/conversations" :aria-label="$t('nav.conversations')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
35-
<div i-ri:at-line />
36-
</NuxtLink>
37-
</template>
38-
<template v-else>
39-
<NuxtLink :to="`/${currentServer}/explore/${lastAccessedExploreRoute}`" :aria-label="$t('nav.explore')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
40-
<div i-ri:compass-3-line />
41-
</NuxtLink>
42-
<NuxtLink group :to="`/${currentServer}/public/local`" :aria-label="$t('nav.local')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
43-
<div i-ri:group-2-line />
44-
</NuxtLink>
45-
<NuxtLink :to="`/${currentServer}/public`" :aria-label="$t('nav.federated')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
46-
<div i-ri:earth-line />
47-
</NuxtLink>
48-
</template>
49-
<NavBottomMoreMenu v-slot="{ toggleVisible, show }" v-model="moreMenuVisible" flex flex-row items-center place-content-center h-full flex-1 cursor-pointer>
50-
<button
51-
flex items-center place-content-center h-full flex-1 class="select-none"
52-
:class="show ? '!text-primary' : ''"
53-
aria-label="More menu"
54-
@click="toggleVisible"
55-
>
56-
<span :class="show ? 'i-ri:close-fill' : 'i-ri:more-fill'" />
57-
</button>
58-
</NavBottomMoreMenu>
43+
<Component :is="navButton!.component" v-for="navButton in selectedNavButtons" :key="navButton!.name" :active-class="moreMenuVisible ? '' : 'text-primary'" />
5944
</nav>
6045
</template>

components/nav/button/Explore.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
import { STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE } from '~/constants'
3+
4+
defineProps<{
5+
activeClass: string
6+
}>()
7+
8+
const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, '')
9+
</script>
10+
11+
<template>
12+
<NuxtLink :to="`/${currentServer}/explore/${lastAccessedExploreRoute}`" :aria-label="$t('nav.explore')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
13+
<div i-ri:compass-3-line />
14+
</NuxtLink>
15+
</template>

components/nav/button/Federated.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
activeClass: string
4+
}>()
5+
</script>
6+
7+
<template>
8+
<NuxtLink :to="`/${currentServer}/public`" :aria-label="$t('nav.federated')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
9+
<div i-ri:earth-line />
10+
</NuxtLink>
11+
</template>

components/nav/button/Home.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
activeClass: string
4+
}>()
5+
</script>
6+
7+
<template>
8+
<NuxtLink to="/home" :aria-label="$t('nav.home')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
9+
<div i-ri:home-5-line />
10+
</NuxtLink>
11+
</template>

components/nav/button/Local.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
activeClass: string
4+
}>()
5+
</script>
6+
7+
<template>
8+
<NuxtLink group :to="`/${currentServer}/public/local`" :aria-label="$t('nav.local')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
9+
<div i-ri:group-2-line />
10+
</NuxtLink>
11+
</template>

components/nav/button/Mention.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
activeClass: string
4+
}>()
5+
</script>
6+
7+
<template>
8+
<NuxtLink
9+
to="/conversations" :aria-label="$t('nav.conversations')"
10+
:active-class="activeClass" flex flex-row items-center place-content-center h-full
11+
flex-1 class="coarse-pointer:select-none" @click="$scrollToTop"
12+
>
13+
<div i-ri:at-line />
14+
</NuxtLink>
15+
</template>

components/nav/button/MoreMenu.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script setup lang="ts">
2+
defineModel<boolean>()
3+
</script>
4+
5+
<template>
6+
<NavBottomMoreMenu
7+
v-slot="{ toggleVisible, show }" v-model="modelValue!" flex flex-row items-center
8+
place-content-center h-full flex-1 cursor-pointer
9+
>
10+
<button
11+
flex items-center place-content-center h-full flex-1 class="select-none"
12+
:class="show ? '!text-primary' : ''"
13+
aria-label="More menu"
14+
@click="toggleVisible"
15+
>
16+
<span :class="show ? 'i-ri:close-fill' : 'i-ri:more-fill'" />
17+
</button>
18+
</NavBottomMoreMenu>
19+
</template>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script setup lang="ts">
2+
import { STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
3+
4+
defineProps<{
5+
activeClass: string
6+
}>()
7+
const { notifications } = useNotifications()
8+
const lastAccessedNotificationRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE, '')
9+
</script>
10+
11+
<template>
12+
<NuxtLink :to="`/notifications/${lastAccessedNotificationRoute}`" :aria-label="$t('nav.notifications')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
13+
<div flex relative>
14+
<div class="i-ri:notification-4-line" text-xl />
15+
<div v-if="notifications" class="top-[-0.3rem] right-[-0.3rem]" absolute font-bold rounded-full h-4 w-4 text-xs bg-primary text-inverted flex items-center justify-center>
16+
{{ notifications < 10 ? notifications : '•' }}
17+
</div>
18+
</div>
19+
</NuxtLink>
20+
</template>

components/nav/button/Search.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
activeClass: string
4+
}>()
5+
</script>
6+
7+
<template>
8+
<NuxtLink to="/search" :aria-label="$t('nav.search')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
9+
<div i-ri:search-line />
10+
</NuxtLink>
11+
</template>
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<script setup lang="ts">
2+
import type { NavButtonName } from '~/composables/settings'
3+
import { STORAGE_KEY_BOTTOM_NAV_BUTTONS } from '~/constants'
4+
5+
interface NavButton {
6+
name: NavButtonName
7+
label: string
8+
icon: string
9+
}
10+
11+
const availableNavButtons: NavButton[] = [
12+
{ name: 'home', label: 'nav.home', icon: 'i-ri:home-5-line' },
13+
{ name: 'search', label: 'nav.search', icon: 'i-ri:search-line' },
14+
{ name: 'notification', label: 'nav.notifications', icon: 'i-ri:notification-4-line' },
15+
{ name: 'mention', label: 'nav.conversations', icon: 'i-ri:at-line' },
16+
{ name: 'explore', label: 'nav.explore', icon: 'i-ri:compass-3-line' },
17+
{ name: 'local', label: 'nav.local', icon: 'i-ri:group-2-line' },
18+
{ name: 'federated', label: 'nav.federated', icon: 'i-ri:earth-line' },
19+
{ name: 'moreMenu', label: 'nav.more_menu', icon: 'i-ri:more-fill' },
20+
] as const
21+
22+
const defaultSelectedNavButtonNames = computed<NavButtonName[]>(() =>
23+
currentUser.value
24+
? ['home', 'search', 'notification', 'mention', 'moreMenu']
25+
: ['explore', 'local', 'federated', 'moreMenu'],
26+
)
27+
const navButtonNamesSetting = useLocalStorage<NavButtonName[]>(STORAGE_KEY_BOTTOM_NAV_BUTTONS, defaultSelectedNavButtonNames.value)
28+
const selectedNavButtonNames = ref(navButtonNamesSetting.value)
29+
30+
const selectedNavButtons = computed<NavButton[]>(() =>
31+
selectedNavButtonNames.value.map(name =>
32+
availableNavButtons.find(navButton => navButton.name === name)!,
33+
),
34+
)
35+
36+
const canSave = computed(() =>
37+
selectedNavButtonNames.value.length > 0
38+
&& selectedNavButtonNames.value.includes('moreMenu')
39+
&& JSON.stringify(selectedNavButtonNames.value) !== JSON.stringify(navButtonNamesSetting.value),
40+
)
41+
42+
function isAdded(name: NavButtonName) {
43+
return selectedNavButtonNames.value.includes(name)
44+
}
45+
46+
function append(navButtonName: NavButtonName) {
47+
const maxButtonNumber = 5
48+
if (selectedNavButtonNames.value.length < maxButtonNumber)
49+
selectedNavButtonNames.value = [...selectedNavButtonNames.value, navButtonName]
50+
}
51+
52+
function remove(navButtonName: NavButtonName) {
53+
selectedNavButtonNames.value = selectedNavButtonNames.value.filter(name => name !== navButtonName)
54+
}
55+
56+
function clear() {
57+
selectedNavButtonNames.value = []
58+
}
59+
60+
function reset() {
61+
selectedNavButtonNames.value = defaultSelectedNavButtonNames.value
62+
}
63+
64+
function save() {
65+
navButtonNamesSetting.value = selectedNavButtonNames.value
66+
}
67+
</script>
68+
69+
<template>
70+
<!-- preview -->
71+
<div flex="~ gap4 wrap" items-center select-settings h-14 p0>
72+
<nav
73+
v-for="availableNavButton in selectedNavButtons" :key="availableNavButton.name"
74+
flex="~ 1" items-center justify-center text-xl
75+
scrollbar-hide overscroll-none
76+
>
77+
<button btn-base :class="availableNavButton.icon" mx-4 tabindex="-1" />
78+
</nav>
79+
</div>
80+
81+
<!-- button selection -->
82+
<div flex="~ gap4 wrap" py4>
83+
<button
84+
v-for="{ name, label, icon } in availableNavButtons"
85+
:key="name"
86+
btn-text flex="~ gap-2" items-center p2 border="~ base rounded" bg-base ws-nowrap
87+
:class="isAdded(name) ? 'text-secondary hover:text-second bg-auto' : ''"
88+
type="button"
89+
role="switch"
90+
:aria-checked="isAdded(name)"
91+
@click="isAdded(name) ? remove(name) : append(name)"
92+
>
93+
<span :class="icon" />
94+
{{ label ? $t(label) : 'More menu' }}
95+
</button>
96+
</div>
97+
98+
<div flex="~ col" gap-y-4 gap-x-2 py-1 sm="~ justify-end flex-row">
99+
<button
100+
btn-outline font-bold py2 full-w sm-wa flex="~ gap2 center"
101+
type="button"
102+
:disabled="selectedNavButtonNames.length === 0"
103+
@click="clear"
104+
>
105+
<span aria-hidden="true" class="block i-ri:delete-bin-line" />
106+
{{ $t('action.clear') }}
107+
</button>
108+
<button
109+
btn-outline font-bold py2 full-w sm-wa flex="~ gap2 center"
110+
type="button"
111+
@click="reset"
112+
>
113+
<span aria-hidden="true" class="block i-ri:repeat-line" />
114+
{{ $t('action.reset') }}
115+
</button>
116+
<button
117+
btn-solid font-bold py2 full-w sm-wa flex="~ gap2 center"
118+
:disabled="!canSave"
119+
@click="save"
120+
>
121+
<span aria-hidden="true" i-ri:save-2-fill />
122+
{{ $t('action.save') }}
123+
</button>
124+
</div>
125+
</template>

composables/settings/definition.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export type OldFontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
77

88
export type ColorMode = 'light' | 'dark' | 'system'
99

10+
export type NavButtonName = 'home' | 'search' | 'notification' | 'mention' | 'explore' | 'local' | 'federated' | 'moreMenu'
11+
1012
export interface PreferencesSettings {
1113
hideAltIndicatorOnPosts: boolean
1214
hideGifIndicatorOnPosts: boolean

constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
2424
export const STORAGE_KEY_PWA_HIDE_INSTALL = 'elk-pwa-hide-install'
2525
export const STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE = 'elk-last-accessed-notification-route'
2626
export const STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE = 'elk-last-accessed-explore-route'
27+
export const STORAGE_KEY_BOTTOM_NAV_BUTTONS = 'elk-bottom-nav-buttons'
2728

2829
export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/
2930

locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"boost": "Boost",
5959
"boost_count": "{0}",
6060
"boosted": "Boosted",
61+
"clear": "Clear",
6162
"clear_publish_failed": "Clear publish errors",
6263
"clear_save_failed": "Clear save errors",
6364
"clear_upload_failed": "Clear file upload errors",
@@ -316,6 +317,7 @@
316317
"list": "List",
317318
"lists": "Lists",
318319
"local": "Local",
320+
"more_menu": "More menu",
319321
"muted_users": "Muted users",
320322
"notifications": "Notifications",
321323
"privacy": "Privacy",
@@ -450,6 +452,8 @@
450452
"label": "Account settings"
451453
},
452454
"interface": {
455+
"bottom_nav": "Bottom Navigation",
456+
"bottom_nav_instructions": "Choose your favorite navigation buttons up to five for the bottom navigation. Must include the \"More menu\" button.",
453457
"color_mode": "Color Mode",
454458
"dark_mode": "Dark",
455459
"default": " (default)",

pages/settings/interface/index.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ useHydratedHead({
3030
</p>
3131
<SettingsThemeColors />
3232
</div>
33+
<div space-y-2>
34+
<p font-medium>
35+
{{ $t('settings.interface.bottom_nav') }}
36+
</p>
37+
<p>
38+
{{ $t('settings.interface.bottom_nav_instructions') }}
39+
</p>
40+
<SettingsBottomNav />
41+
</div>
3342
</div>
3443
</MainContent>
3544
</template>

0 commit comments

Comments
 (0)