💄 Better layout design
This commit is contained in:
		
							
								
								
									
										41
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,39 +1,6 @@ | ||||
| # @hydrogen/solaragent | ||||
| # SolarAgent | ||||
|  | ||||
| This template should help get you started developing with Vue 3 in Vite. | ||||
| Hola! This is Project Hydrogen's universal frontend. | ||||
| Integrated support for Identity, Interactive and Messaging! | ||||
|  | ||||
| ## Recommended IDE Setup | ||||
|  | ||||
| [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). | ||||
|  | ||||
| ## Type Support for `.vue` Imports in TS | ||||
|  | ||||
| TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. | ||||
|  | ||||
| ## Customize configuration | ||||
|  | ||||
| See [Vite Configuration Reference](https://vitejs.dev/config/). | ||||
|  | ||||
| ## Project Setup | ||||
|  | ||||
| ```sh | ||||
| bun install | ||||
| ``` | ||||
|  | ||||
| ### Compile and Hot-Reload for Development | ||||
|  | ||||
| ```sh | ||||
| bun dev | ||||
| ``` | ||||
|  | ||||
| ### Type-Check, Compile and Minify for Production | ||||
|  | ||||
| ```sh | ||||
| bun build | ||||
| ``` | ||||
|  | ||||
| ### Lint with [ESLint](https://eslint.org/) | ||||
|  | ||||
| ```sh | ||||
| bun lint | ||||
| ``` | ||||
| Also provide a mobile version that powered by capacitor! | ||||
| @@ -20,6 +20,7 @@ | ||||
|     "@capacitor/preferences": "^5.0.7", | ||||
|     "@fontsource/roboto": "^5.0.12", | ||||
|     "@mdi/font": "^7.4.47", | ||||
|     "@vueuse/core": "^10.9.0", | ||||
|     "dayjs": "^1.11.10", | ||||
|     "dompurify": "^3.0.11", | ||||
|     "marked": "^12.0.1", | ||||
|   | ||||
							
								
								
									
										152
									
								
								src/components/navigation/NavigationDrawer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								src/components/navigation/NavigationDrawer.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| <template> | ||||
|   <v-navigation-drawer | ||||
|     v-model="ui.drawer.open" | ||||
|     color="grey-lighten-5" | ||||
|     width="320" | ||||
|     :permanent="isLargeScreen" | ||||
|     :rail="ui.drawer.mini" | ||||
|     :rail-width="58" | ||||
|     :order="0" | ||||
|     floating | ||||
|     @click="ui.drawer.mini = false" | ||||
|   > | ||||
|     <div class="flex flex-col h-full"> | ||||
|       <v-toolbar | ||||
|         class="flex items-center justify-between px-[14px] border-opacity-15" | ||||
|         color="primary" | ||||
|         height="64" | ||||
|         :style="`padding-top: ${safeAreaTop}`" | ||||
|       > | ||||
|         <div class="flex items-center"> | ||||
|           <img src="/favicon.png" alt="Logo" width="32" height="32" class="block" /> | ||||
|           <div v-show="!ui.drawer.mini" class="ms-6 font-medium">Solar Network</div> | ||||
|         </div> | ||||
|  | ||||
|         <v-spacer /> | ||||
|  | ||||
|         <div> | ||||
|           <v-btn | ||||
|             v-if="isLargeScreen" | ||||
|             v-show="!ui.drawer.mini" | ||||
|             icon="mdi-arrow-collapse-left" | ||||
|             size="small" | ||||
|             variant="text" | ||||
|             @click.stop="ui.drawer.mini = true" | ||||
|           /> | ||||
|         </div> | ||||
|       </v-toolbar> | ||||
|  | ||||
|       <div class="flex-grow-1"> | ||||
|         <v-list | ||||
|           class="nav-list" | ||||
|           density="compact" | ||||
|           :opened="ui.drawer.mini ? [] : expanded.nav" | ||||
|           @update:opened="(val) => expanded.nav = val" | ||||
|         > | ||||
|           <v-list-item | ||||
|             exact | ||||
|             title="Explore" | ||||
|             prepend-icon="mdi-compass" | ||||
|             :to="{ name: 'explore' }" | ||||
|           /> | ||||
|         </v-list> | ||||
|  | ||||
|         <v-divider class="border-opacity-75 my-2" /> | ||||
|  | ||||
|         <v-list | ||||
|           class="resources-list" | ||||
|           density="compact" | ||||
|           :opened="ui.drawer.mini ? [] : expanded.resources" | ||||
|           @update:opened="(val) => expanded.resources = val" | ||||
|         > | ||||
|           <channel-list /> | ||||
|           <realm-list /> | ||||
|         </v-list> | ||||
|       </div> | ||||
|  | ||||
|       <!-- User info --> | ||||
|       <v-list | ||||
|         class="border-opacity-15 h-[64px]" | ||||
|         style="border-top-width: thin" | ||||
|         :style="`margin-bottom: ${safeAreaBottom}`" | ||||
|       > | ||||
|         <v-list-item :subtitle="username" :title="nickname"> | ||||
|           <template #prepend> | ||||
|             <v-avatar icon="mdi-account-circle" :image="id.userinfo.data?.picture" /> | ||||
|           </template> | ||||
|           <template #append> | ||||
|             <div v-if="id.userinfo.isLoggedIn"> | ||||
|               <notification-list /> | ||||
|               <user-menu /> | ||||
|             </div> | ||||
|           </template> | ||||
|         </v-list-item> | ||||
|       </v-list> | ||||
|     </div> | ||||
|   </v-navigation-drawer> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { computed, onMounted, reactive, ref } from "vue" | ||||
| import { useUserinfo } from "@/stores/userinfo" | ||||
| import { useWellKnown } from "@/stores/wellKnown" | ||||
| import { useUI } from "@/stores/ui" | ||||
| import { useRealms } from "@/stores/realms" | ||||
| import { useChannels } from "@/stores/channels" | ||||
| import { useMediaQuery } from "@vueuse/core" | ||||
| import PullToRefresh from "pulltorefreshjs" | ||||
| import UserMenu from "@/components/users/UserMenu.vue" | ||||
| import RealmList from "@/components/realms/RealmList.vue" | ||||
| import ChannelList from "@/components/chat/channels/ChannelList.vue" | ||||
| import NotificationList from "@/components/users/NotificationList.vue" | ||||
|  | ||||
| const ui = useUI() | ||||
| const expanded = reactive<{ [id: string]: string[] }>({ | ||||
|   nav: [], | ||||
|   resources: ["channels", "realms"] | ||||
| }) | ||||
|  | ||||
| const isLargeScreen = useMediaQuery("(min-width: 768px)") | ||||
|  | ||||
| const safeAreaTop = computed(() => { | ||||
|   return `${ui.safeArea.top}px` | ||||
| }) | ||||
|  | ||||
| const safeAreaBottom = computed(() => { | ||||
|   return `${ui.safeArea.bottom}px` | ||||
| }) | ||||
|  | ||||
| const id = useUserinfo() | ||||
|  | ||||
| const username = computed(() => { | ||||
|   if (id.userinfo.isLoggedIn) { | ||||
|     return "@" + id.userinfo.data?.name | ||||
|   } else { | ||||
|     return "@vistor" | ||||
|   } | ||||
| }) | ||||
| const nickname = computed(() => { | ||||
|   if (id.userinfo.isLoggedIn) { | ||||
|     return id.userinfo.data?.nick | ||||
|   } else { | ||||
|     return "Anonymous" | ||||
|   } | ||||
| }) | ||||
|  | ||||
| id.readProfiles() | ||||
|  | ||||
| useWellKnown().readWellKnown() | ||||
|  | ||||
| onMounted(() => { | ||||
|   PullToRefresh.init({ | ||||
|     mainElement: ".resources-list", | ||||
|     triggerElement: ".resources-list", | ||||
|     onRefresh() { | ||||
|       return Promise.all([ | ||||
|         useRealms().list(), | ||||
|         useChannels().list() | ||||
|       ]) | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
| </script> | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <v-menu eager :close-on-content-click="false"> | ||||
|   <v-menu location="top" :close-on-content-click="false"> | ||||
|     <template #activator="{ props }"> | ||||
|       <v-btn v-bind="props" icon size="small" variant="text" :loading="loading"> | ||||
|       <v-btn v-bind="props" icon size="small" color="teal" variant="text" :loading="loading"> | ||||
|         <v-badge v-if="notify.total > 0" color="error" :content="notify.total"> | ||||
|           <v-icon icon="mdi-bell" /> | ||||
|         </v-badge> | ||||
|   | ||||
| @@ -4,12 +4,12 @@ | ||||
|       <v-btn icon="mdi-menu-up" size="small" variant="text" v-bind="props" /> | ||||
|     </template> | ||||
|  | ||||
|     <v-list density="compact" v-if="!id.userinfo.isLoggedIn"> | ||||
|     <v-list class="w-[280px]" density="compact" v-if="!id.userinfo.isLoggedIn"> | ||||
|       <v-list-item title="Sign in" prepend-icon="mdi-login-variant" :to="{ name: 'auth.sign-in' }" /> | ||||
|       <v-list-item title="Create account" prepend-icon="mdi-account-plus" :to="{ name: 'auth.sign-up' }" /> | ||||
|     </v-list> | ||||
|  | ||||
|     <v-list density="compact" v-else> | ||||
|     <v-list class="w-[280px]" density="compact" v-else> | ||||
|       <v-list-item title="Settings" prepend-icon="mdi-cog" exact :to="{ name: 'settings' }" /> | ||||
|  | ||||
|       <v-divider class="border-opacity-50 my-2" /> | ||||
|   | ||||
| @@ -1,83 +1,15 @@ | ||||
| <template> | ||||
|   <v-navigation-drawer | ||||
|     v-model="drawerOpen" | ||||
|     color="grey-lighten-5" | ||||
|     width="320" | ||||
|     :rail="drawerMini" | ||||
|     :rail-width="58" | ||||
|     :order="0" | ||||
|     floating | ||||
|     @click="drawerMini = false" | ||||
|   > | ||||
|     <div class="flex flex-col h-full"> | ||||
|       <v-toolbar | ||||
|         class="flex items-center justify-between px-[14px] border-opacity-15" | ||||
|         color="primary" | ||||
|         height="64" | ||||
|         :style="`padding-top: ${safeAreaTop}`" | ||||
|       > | ||||
|         <div class="flex items-center"> | ||||
|           <img src="/favicon.png" alt="Logo" width="32" height="32" class="block" /> | ||||
|           <div v-show="!drawerMini" class="ms-6 font-medium">Solar Network</div> | ||||
|         </div> | ||||
|   <NavigationDrawer /> | ||||
|  | ||||
|         <v-spacer /> | ||||
|  | ||||
|         <div> | ||||
|           <v-btn | ||||
|             v-show="!drawerMini" | ||||
|             icon="mdi-arrow-collapse-left" | ||||
|             size="small" | ||||
|             variant="text" | ||||
|             @click.stop="drawerMini = true" | ||||
|           /> | ||||
|         </div> | ||||
|       </v-toolbar> | ||||
|  | ||||
|       <div class="flex-grow-1"> | ||||
|         <v-list | ||||
|           class="resources-list" | ||||
|           density="compact" | ||||
|           :opened="drawerMini ? [] : expanded" | ||||
|           @update:opened="(val) => expanded = val" | ||||
|         > | ||||
|           <channel-list /> | ||||
|           <v-divider class="border-opacity-75 my-2" /> | ||||
|           <realm-list /> | ||||
|         </v-list> | ||||
|       </div> | ||||
|  | ||||
|       <!-- User info --> | ||||
|       <v-list | ||||
|         class="border-opacity-15 h-[64px]" | ||||
|         style="border-top-width: thin" | ||||
|         :style="`margin-bottom: ${safeAreaBottom}`" | ||||
|       > | ||||
|         <v-list-item :subtitle="username" :title="nickname"> | ||||
|           <template #prepend> | ||||
|             <v-avatar icon="mdi-account-circle" :image="id.userinfo.data?.picture" /> | ||||
|           </template> | ||||
|           <template #append> | ||||
|             <user-menu /> | ||||
|           </template> | ||||
|         </v-list-item> | ||||
|       </v-list> | ||||
|     </div> | ||||
|   </v-navigation-drawer> | ||||
|  | ||||
|   <v-app-bar height="64" color="primary" scroll-behavior="hide" :order="2" flat> | ||||
|   <v-app-bar v-if="!isLargeScreen" height="64" color="primary" scroll-behavior="hide" :order="2" flat> | ||||
|     <div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center"> | ||||
|       <v-app-bar-nav-icon variant="text" @click.stop="drawerOpen = !drawerOpen" /> | ||||
|       <v-app-bar-nav-icon variant="text" @click.stop="ui.drawer.open = !ui.drawer.open" /> | ||||
|  | ||||
|       <router-link :to="{ name: 'explore' }"> | ||||
|         <h2 class="ml-2 text-lg font-500">Solian</h2> | ||||
|       </router-link> | ||||
|  | ||||
|       <v-spacer /> | ||||
|  | ||||
|       <div v-if="id.userinfo.isLoggedIn"> | ||||
|         <notification-list /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </v-app-bar> | ||||
|  | ||||
| @@ -87,64 +19,17 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { computed, onMounted, ref } from "vue" | ||||
| import { useUserinfo } from "@/stores/userinfo" | ||||
| import { useWellKnown } from "@/stores/wellKnown" | ||||
| import { useMediaQuery } from "@vueuse/core" | ||||
| import { useUI } from "@/stores/ui" | ||||
| import { useRealms } from "@/stores/realms" | ||||
| import { useChannels } from "@/stores/channels" | ||||
| import RealmList from "@/components/realms/RealmList.vue" | ||||
| import NotificationList from "@/components/users/NotificationList.vue" | ||||
| import ChannelList from "@/components/chat/channels/ChannelList.vue" | ||||
| import UserMenu from "@/components/users/UserMenu.vue" | ||||
| import PullToRefresh from "pulltorefreshjs" | ||||
| import NavigationDrawer from "@/components/navigation/NavigationDrawer.vue" | ||||
|  | ||||
| const ui = useUI() | ||||
| const expanded = ref<string[]>(["channels"]) | ||||
|  | ||||
| const safeAreaTop = computed(() => { | ||||
|   return `${ui.safeArea.top}px` | ||||
| }) | ||||
|  | ||||
| const safeAreaBottom = computed(() => { | ||||
|   return `${ui.safeArea.bottom}px` | ||||
| }) | ||||
|  | ||||
| const id = useUserinfo() | ||||
|  | ||||
| const username = computed(() => { | ||||
|   if (id.userinfo.isLoggedIn) { | ||||
|     return "@" + id.userinfo.data?.name | ||||
|   } else { | ||||
|     return "@vistor" | ||||
|   } | ||||
| }) | ||||
| const nickname = computed(() => { | ||||
|   if (id.userinfo.isLoggedIn) { | ||||
|     return id.userinfo.data?.nick | ||||
|   } else { | ||||
|     return "Anonymous" | ||||
|   } | ||||
| }) | ||||
|  | ||||
| id.readProfiles() | ||||
| const isLargeScreen = useMediaQuery("(min-width: 768px)") | ||||
|  | ||||
| useUserinfo().readProfiles() | ||||
| useWellKnown().readWellKnown() | ||||
|  | ||||
| const drawerOpen = ref(true) | ||||
| const drawerMini = ref(false) | ||||
|  | ||||
| onMounted(() => { | ||||
|   PullToRefresh.init({ | ||||
|     mainElement: ".resources-list", | ||||
|     triggerElement: ".resources-list", | ||||
|     onRefresh() { | ||||
|       return Promise.all([ | ||||
|         useRealms().list(), | ||||
|         useChannels().list() | ||||
|       ]) | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,11 @@ export const useUI = defineStore("ui", () => { | ||||
|     messages: false, | ||||
|   }) | ||||
|  | ||||
|   const drawer = reactive({ | ||||
|     open: true, | ||||
|     mini: false, | ||||
|   }) | ||||
|  | ||||
|   const safeArea = reactive({ | ||||
|     top: 0, | ||||
|     bottom: 0 | ||||
| @@ -27,5 +32,5 @@ export const useUI = defineStore("ui", () => { | ||||
|    }, 5000) | ||||
|   } | ||||
|  | ||||
|   return { safeArea, snackbar, reconnection, showSnackbar, showErrorSnackbar } | ||||
|   return { safeArea, snackbar, drawer, reconnection, showSnackbar, showErrorSnackbar } | ||||
| }) | ||||
|   | ||||
| @@ -45,8 +45,10 @@ import { computed, onUnmounted, reactive, ref, watch } from "vue" | ||||
| import { useRoute } from "vue-router" | ||||
| import ChatList from "@/components/chat/ChatList.vue" | ||||
| import ChatEditor from "@/components/chat/ChatEditor.vue" | ||||
| import { useUserinfo } from "@/stores/userinfo" | ||||
|  | ||||
| const ui = useUI() | ||||
| const id = useUserinfo() | ||||
| const route = useRoute() | ||||
| const channels = useChannels() | ||||
|  | ||||
| @@ -154,17 +156,28 @@ let mounted = false | ||||
| let jitsiInstance: any | ||||
|  | ||||
| async function mountJitsi() { | ||||
|   joining.value = true | ||||
|   if (mounted) return false | ||||
|   if (!channels.call) return | ||||
|  | ||||
|   joining.value = true | ||||
|   const tk = await channels.exchangeCallToken() | ||||
|   joining.value = false | ||||
|   if (!tk) return | ||||
|  | ||||
|   const domain = tk.endpoint.replace("http://", "").replace("https://", "") | ||||
|   const options = { | ||||
|     roomName: channels.call.external_id, | ||||
|     parentNode: document.querySelector("#call"), | ||||
|     onload: () => joining.value = false, | ||||
|     jwt: tk.token, | ||||
|     userInfo: { | ||||
|       avatar: id.userinfo.data?.picture, | ||||
|       displayName: id.userinfo.displayName, | ||||
|     }, | ||||
|     configOverwrite: { | ||||
|       prejoinPageEnabled: true, | ||||
|       startWithAudioMuted: false, | ||||
|       startWithVideoMuted: true | ||||
|     } | ||||
|   } | ||||
|   // This class imported by the script tag in index.html | ||||
|   // @ts-ignore | ||||
|   | ||||
		Reference in New Issue
	
	Block a user