34 Commits

Author SHA1 Message Date
LittleSheep
f8a838f5d7 🚑 Fix cannot click send button 2024-04-11 22:57:17 +08:00
LittleSheep
ff55062850 💄 Better chatting ui
🐛 Fix multiple connections
2024-04-11 22:37:44 +08:00
LittleSheep
c94dd8b761 ⬆️ Fix remote deps loading issue 2024-04-08 23:28:07 +08:00
LittleSheep
06f8b9da85 💄 Better layout design 2024-04-08 23:14:09 +08:00
LittleSheep
bbe6dbb2ca Support only attachments messages and markdown messages 2024-04-06 23:45:24 +08:00
LittleSheep
3c02691511 🐛 Bug fixes of friends and voice chat 2024-04-06 23:36:10 +08:00
LittleSheep
76367bbd25 Voice Chat yo! 2024-04-06 23:10:09 +08:00
LittleSheep
8eb28f0115 Friend invitation auto complete 2024-04-06 15:10:06 +08:00
LittleSheep
79cd1129fd 🐛 Fix wrong can approve detection 2024-04-06 03:10:58 +08:00
LittleSheep
4c929a14fa 🐛 Fix wrong binding with approve and decline 2024-04-06 03:05:06 +08:00
LittleSheep
2d3f8a8bd7 Friends 2024-04-06 02:08:57 +08:00
LittleSheep
cbcb007517 Security page 2024-04-06 01:23:55 +08:00
LittleSheep
f5603ad884 🐛 Fix button position mismatch 2024-04-05 22:07:40 +08:00
LittleSheep
202b6c1a10 🐛 Fix infinite reconnect issue 2024-04-05 21:48:53 +08:00
LittleSheep
c1f42ed4f7 Show time and id at messages 2024-04-05 21:07:55 +08:00
LittleSheep
634347a958 🚑 Fix chat message issue 2024-04-05 16:04:25 +08:00
LittleSheep
9039dfb34e 🚨 Fix tsc check 2024-04-05 13:30:57 +08:00
LittleSheep
0b24b7cc05 Fix keybinding in chatting 2024-04-05 13:29:53 +08:00
LittleSheep
4e4bc3345d Pull to refresh 2024-04-05 13:20:01 +08:00
LittleSheep
4a2ff8fce6 🐛 Fix request bug 2024-04-05 13:03:29 +08:00
LittleSheep
3a42c58013 🐛 Fix cannot read image in some browser 2024-04-05 11:45:54 +08:00
LittleSheep
b6f50bbf53 🐛 Fix isOwned issue 2024-04-05 00:57:04 +08:00
LittleSheep
21b2f1e555 🐛 Fix notifications 2024-04-04 23:23:41 +08:00
LittleSheep
7e01edffbe Settings page 2024-04-04 22:36:04 +08:00
LittleSheep
054e349e6b Image cropper and account settings 2024-04-04 22:28:15 +08:00
LittleSheep
9bc387cb86 🎨 Deconstruct the snackbar 2024-04-04 18:35:48 +08:00
LittleSheep
49bd6ea363 🐛 Websocket auto reconnecting 2024-04-02 23:31:24 +08:00
LittleSheep
9ad11f4297 🐛 Fix some style issue 2024-04-02 23:29:28 +08:00
LittleSheep
8af78a26ba 🚨 Fix typescript lint 2024-04-02 23:20:21 +08:00
LittleSheep
031c3dee3b User personal page 2024-04-02 23:17:57 +08:00
LittleSheep
1f3f4a7370 🐛 Bug fixes of chatting message misplace 2024-04-01 22:07:48 +08:00
LittleSheep
e36fc53df8 🚀 Configure for android 2024-04-01 21:01:53 +08:00
LittleSheep
280a180d9e Add progress bar 2024-04-01 20:29:04 +08:00
LittleSheep
509d433959 Bug fixes 2024-04-01 20:21:01 +08:00
116 changed files with 2163 additions and 784 deletions

View File

@@ -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!

View File

@@ -1,46 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<application
android:allowBackup="true"
android:label="Solian"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBarLaunch">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
<!-- Permissions -->
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -5,166 +5,6 @@
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:fillColor="#FFFFFF"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -17,6 +17,5 @@
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@@ -5,13 +5,8 @@
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" type="image/png" href="/apple-touch-icon.png" sizes="1024x1024">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<script src="https://meet.element.io/external_api.js"></script>
<title>Solian</title>
<style>
html, body {
scroll-behavior: smooth;
}
</style>
</head>
<body>
<div id="app"></div>

View File

@@ -20,11 +20,15 @@
"@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",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"universal-cookie": "^7.1.0",
"vue": "^3.4.21",
"vue-advanced-cropper": "^2.8.8",
"vue-easy-lightbox": "^1.19.0",
"vue-router": "^4.3.0",
"vuetify": "^3.5.12"
@@ -36,6 +40,8 @@
"@tsconfig/node20": "^20.1.2",
"@types/dompurify": "^3.0.5",
"@types/node": "^20.11.28",
"@types/nprogress": "^0.2.3",
"@types/pulltorefreshjs": "^0.1.7",
"@unocss/reset": "^0.58.7",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -46,6 +52,7 @@
"eslint-plugin-vue": "^9.17.0",
"npm-run-all2": "^6.1.2",
"prettier": "^3.0.3",
"pulltorefreshjs": "^0.1.22",
"typescript": "~5.4.0",
"unocss": "^0.58.7",
"vite": "^5.1.6",

View File

@@ -11,4 +11,13 @@ body,
.no-scrollbar::-webkit-scrollbar {
width: 0;
display: none;
}
html, body {
scroll-behavior: smooth;
}
#nprogress .bar {
background: #ffffff !important;
}

View File

@@ -1,43 +0,0 @@
<template>
<v-menu>
<template #activator="{ props }">
<v-btn flat exact v-bind="props" icon>
<v-avatar color="transparent" icon="mdi-account-circle" :image="'/api/avatar/' + id.userinfo.data?.avatar" />
</v-btn>
</template>
<v-list 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-item :title="nickname" :subtitle="username" />
<v-divider class="border-opacity-50 my-2" />
<v-list-item title="User Center" prepend-icon="mdi-account-supervisor" exact :to="{ name: 'dashboard' }" />
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import { useUserinfo } from "@/stores/userinfo"
import { computed } from "vue"
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"
}
})
</script>

View File

@@ -1,10 +1,10 @@
<template>
<v-form class="flex-grow-1" ref="chat" @submit.prevent="sendMessage">
<v-form ref="chat" @submit.prevent="sendMessage">
<v-expand-transition>
<v-alert
v-show="channels.related?.messages?.reply_to"
class="mb-3 text-sm"
variant="tonal"
class="mb-2 text-sm"
variant="elevated"
density="compact"
type="info"
>
@@ -20,7 +20,7 @@
<v-btn
icon="mdi-close"
size="x-small"
color="info"
color="white"
variant="text"
@click="channels.related.messages.reply_to = null"
/>
@@ -31,8 +31,8 @@
<v-expand-transition>
<v-alert
v-show="channels.related?.messages?.edit_to"
class="mb-3 text-sm"
variant="tonal"
class="mb-2 text-sm"
variant="elevated"
density="compact"
type="info"
>
@@ -48,7 +48,7 @@
<v-btn
icon="mdi-close"
size="x-small"
color="info"
color="white"
variant="text"
@click="channels.related.messages.edit_to = null"
/>
@@ -60,18 +60,16 @@
auto-grow
hide-details
class="w-full"
variant="outlined"
density="compact"
density="comfortable"
placeholder="Enter some messages..."
:rows="1"
:max-rows="6"
:loading="loading"
v-model="data.content"
@keyup.ctrl.enter="sendMessage"
@keyup.meta.enter="sendMessage"
@keydown="onEditorKeydown"
@paste="pasteMedia"
>
<template #append>
<template #append-inner>
<v-btn
icon
type="button"
@@ -79,7 +77,7 @@
size="small"
variant="text"
:disabled="loading"
@click="dialogs.attachments = true"
@click.stop="dialogs.attachments = true"
>
<v-badge v-if="data.attachments.length > 0" :content="data.attachments.length">
<v-icon icon="mdi-paperclip" />
@@ -88,7 +86,7 @@
<v-icon v-else icon="mdi-paperclip" />
</v-btn>
<v-btn type="submit" icon="mdi-send" size="small" variant="text" :disabled="loading" />
<v-btn type="submit" icon="mdi-send" size="small" variant="text" @click.stop :disabled="loading" />
</template>
</v-textarea>
@@ -103,9 +101,6 @@
Uploading your media, please stand by...
<v-progress-linear class="snackbar-progress" indeterminate />
</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</v-form>
</template>
@@ -114,15 +109,15 @@ import { reactive, ref, watch } from "vue"
import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
import { useChannels } from "@/stores/channels"
import { useUI } from "@/stores/ui"
import Attachments from "@/components/chat/parts/ChatAttachments.vue"
import Media from "@/components/publish/parts/PublishMedia.vue"
const emits = defineEmits(["sent"])
const chat = ref<HTMLFormElement>()
const channels = useChannels()
const error = ref<string | null>(null)
const { showErrorSnackbar } = useUI()
const uploading = ref(false)
const loading = ref(false)
@@ -139,7 +134,7 @@ const data = ref<any>({
})
async function sendMessage() {
if (!data.value.content) return
if (!data.value.content && !data.value.attachments) return
const url = channels.related.messages.edit_to
? `/api/channels/${channels.current.alias}/messages/${channels.related.messages.edit_to?.id}`
@@ -156,20 +151,27 @@ async function sendMessage() {
body: JSON.stringify(payload)
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
emits("sent")
resetEditor()
error.value = null
}
loading.value = false
}
function onEditorKeydown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "enter") {
sendMessage()
}
}
watch(
() => channels.related.messages.reply_to,
(val) => {
if (val) {
data.value.reply_id = val.id
} else {
data.value.reply_id = null
}
}
)
@@ -179,6 +181,8 @@ watch(
(val) => {
if (val) {
data.value = val
} else {
resetEditor()
}
}
)
@@ -203,10 +207,3 @@ function pasteMedia(evt: ClipboardEvent) {
}
}
</script>
<style>
.snackbar-progress {
margin: 12px -16px -14px;
width: calc(100% + 64px);
}
</style>

View File

@@ -2,11 +2,9 @@
<div class="relative transition-colors transition-300 message-item">
<a v-if="props.item?.reply_to" :href="`#m${props.item?.reply_to.id}`">
<div class="pl-2 mb-0.5 text-sm opacity-80 flex items-center">
<v-icon icon="mdi-reply" class="me-2" />
<v-icon icon="mdi-reply" class="me-2 mb-1" />
<v-avatar size="18" class="me-1.5" :image="replyingFromPicture"></v-avatar>
<span class="me-1 text-xs overflow-hidden ws-nowarp text-ellipsis">{{ props.item?.reply_to?.content }}</span>
<span class="text-xs overflow-hidden ws-nowarp text-ellipsis">
from {{ props.item?.reply_to?.sender.account.name }}
</span>
</div>
</a>
@@ -21,8 +19,17 @@
</div>
<div class="flex-grow-1">
<div class="font-bold text-sm">{{ props.item?.sender.account.nick }}</div>
<div>{{ props.item?.content }}</div>
<div class="flex gap-1.25 text-sm items-baseline">
<span class="font-bold">{{ props.item?.sender.account.nick }}</span>
<span class="opacity-80">{{ createdAt }}</span>
<span class="opacity-60 text-xs">#{{ props.item?.id }}</span>
</div>
<div
v-if="props.item?.content"
class="prose prose-message max-w-none"
v-html="parseContent(props.item?.content ?? '')"
/>
<message-attachment
v-if="props.item?.attachments && props.item?.attachments.length > 0"
@@ -35,8 +42,10 @@
<v-card>
<div class="flex px-2 py-0.5">
<v-btn icon="mdi-reply" size="x-small" variant="text" @click="replyMessage" />
<v-btn icon="mdi-pencil" size="x-small" variant="text" color="warning" @click="editMessage" />
<v-btn icon="mdi-delete" size="x-small" variant="text" color="error" @click="deleteMessage" />
<v-btn v-if="isOwned" icon="mdi-pencil" size="x-small" variant="text" color="warning"
@click="editMessage" />
<v-btn v-if="isOwned" icon="mdi-delete" size="x-small" variant="text" color="error"
@click="deleteMessage" />
</div>
</v-card>
</div>
@@ -46,12 +55,29 @@
<script setup lang="ts">
import { useChannels } from "@/stores/channels"
import { useUserinfo } from "@/stores/userinfo"
import { computed } from "vue"
import { parse } from "marked"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import dompurify from "dompurify"
import MessageAttachment from "@/components/chat/renderer/MessageAttachment.vue"
const id = useUserinfo()
const channels = useChannels()
const props = defineProps<{ item: any }>()
dayjs.extend(relativeTime)
const isOwned = computed(() => props.item?.sender?.account_id === id.userinfo.idSet.messaging)
const createdAt = computed(() => dayjs(props.item?.created_at).fromNow())
const replyingFromPicture = computed(() => props.item?.reply_to.sender.account?.avatar ?
props.item?.reply_to.sender.account?.avatar :
null
)
function replyMessage() {
channels.related.messages.reply_to = JSON.parse(JSON.stringify(props.item))
}
@@ -65,6 +91,10 @@ function deleteMessage() {
channels.related.messages.delete_to.channel = channels.current
channels.show.messages.delete = true
}
function parseContent(src: string): string {
return dompurify().sanitize(parse(src) as string)
}
</script>
<style scoped>
@@ -75,7 +105,7 @@ function deleteMessage() {
.message-action {
position: absolute;
right: 8px;
top: -25%;
top: -18px;
opacity: 0;
}
@@ -87,3 +117,9 @@ function deleteMessage() {
opacity: 100%;
}
</style>
<style>
.prose.prose-message p {
margin: 0 !important;
}
</style>

View File

@@ -12,11 +12,6 @@
</div>
</template>
</v-card>
<v-snackbar v-model="success" :timeout="3000">The realm has been deleted.</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
@@ -24,11 +19,11 @@ import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
import { useChannels } from "@/stores/channels"
import { ref } from "vue"
import { useUI } from "@/stores/ui"
const channels = useChannels()
const error = ref<string | null>(null)
const success = ref(false)
const { showSnackbar, showErrorSnackbar } = useUI()
const loading = ref(false)
async function deleteMessage() {
@@ -41,9 +36,9 @@ async function deleteMessage() {
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
success.value = true
showSnackbar("The message has been deleted.")
channels.show.messages.delete = false
channels.related.messages.delete_to = null
}

View File

@@ -1,14 +1,14 @@
<template>
<v-menu>
<template #activator="{ props }">
<v-btn v-bind="props" icon="mdi-cog" variant="text" />
<v-btn v-bind="props" icon="mdi-cog" size="small" variant="text" />
</template>
<v-list density="compact" lines="one">
<v-list-item disabled append-icon="mdi-flag" title="Report" />
<v-list-item v-if="isOwned" append-icon="mdi-pencil" title="Edit" @click="editChannel" />
<v-list-item v-if="isOwned" append-icon="mdi-account-supervisor-circle" title="Members" @click="manageChannel" />
<v-list-item v-if="isOwned" append-icon="mdi-delete" title="Delete" @click="deleteChannel" />
<v-list-item v-if="isOwned" append-icon="mdi-account-supervisor-circle" title="Members" @click="manageChannel" />
</v-list>
</v-menu>
</template>

View File

@@ -12,11 +12,6 @@
</div>
</template>
</v-card>
<v-snackbar v-model="success" :timeout="3000">The realm has been deleted.</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
@@ -25,6 +20,7 @@ import { getAtk } from "@/stores/userinfo"
import { useChannels } from "@/stores/channels"
import { useRoute, useRouter } from "vue-router"
import { ref } from "vue"
import { useUI } from "@/stores/ui"
const route = useRoute()
const router = useRouter()
@@ -32,8 +28,7 @@ const channels = useChannels()
const emits = defineEmits(["relist"])
const error = ref<string | null>(null)
const success = ref(false)
const { showSnackbar, showErrorSnackbar } = useUI()
const loading = ref(false)
async function deleteChannel() {
@@ -46,9 +41,9 @@ async function deleteChannel() {
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
success.value = true
showSnackbar("The channel has been deleted.")
channels.show.delete = false
channels.related.delete_to = null
emits("relist")

View File

@@ -15,9 +15,6 @@
</v-card-actions>
</v-form>
</v-card>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
@@ -25,12 +22,13 @@ import { ref, watch } from "vue"
import { getAtk } from "@/stores/userinfo"
import { request } from "@/scripts/request"
import { useChannels } from "@/stores/channels"
import { useUI } from "@/stores/ui"
const emits = defineEmits(["relist"])
const channels = useChannels()
const error = ref<null | string>(null)
const {showErrorSnackbar} = useUI()
const loading = ref(false)
const data = ref({
@@ -54,7 +52,7 @@ async function submit(evt: SubmitEvent) {
body: JSON.stringify(payload)
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
emits("relist")
form.reset()

View File

@@ -2,11 +2,13 @@
<v-card prepend-icon="mdi-account-plus" title="Invite someone">
<v-form @submit.prevent="inviteMember">
<v-card-text>
<v-text-field
<v-autocomplete
label="Username"
variant="outlined"
density="comfortable"
hint="Require username not the nickname"
autocomplete="off"
hide-selected
:items="friends.available.map(x => getOtherside(x).name)"
v-model="targetName"
/>
</v-card-text>
@@ -23,11 +25,15 @@
<script setup lang="ts">
import { ref } from "vue"
import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
import { getAtk, useUserinfo } from "@/stores/userinfo"
import { useFriends } from "@/stores/friends"
const props = defineProps<{ item: any }>()
const emits = defineEmits(["close", "error", "relist"])
const id = useUserinfo()
const friends = useFriends()
const loading = ref(false)
const targetName = ref("")
@@ -51,4 +57,12 @@ async function inviteMember(evt: SubmitEvent) {
}
loading.value = false
}
function getOtherside(item: any) {
if (item.account_id === id.userinfo.data?.id) {
return item.related
} else {
return item.account
}
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<v-list-group value="channels">
<v-list-group class="channels-list" value="channels">
<template #activator="{ props }">
<v-list-item
v-bind="props"
@@ -28,7 +28,6 @@
<script setup lang="ts">
import { useUserinfo } from "@/stores/userinfo"
import { useRealms } from "@/stores/realms"
import { useChannels } from "@/stores/channels"
const id = useUserinfo()

View File

@@ -31,23 +31,20 @@
<div class="px-3">
<v-dialog class="max-w-[540px]">
<template #activator="{ props }">
<v-btn v-bind="props" block prepend-icon="mdi-account-plus" variant="plain"> Invite someone </v-btn>
<v-btn v-bind="props" block prepend-icon="mdi-account-plus" variant="plain"> Invite someone</v-btn>
</template>
<template #default="{ isActive }">
<channel-invitation
:item="props.item"
@relist="listMembers"
@error="(val) => (error = val)"
@error="(val) => (showErrorSnackbar(val))"
@close="isActive.value = false"
/>
</template>
</v-dialog>
</div>
</div>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</v-card>
</template>
@@ -57,6 +54,7 @@ import { request } from "@/scripts/request"
import { getAtk, useUserinfo } from "@/stores/userinfo"
import { computed } from "vue"
import ChannelInvitation from "@/components/chat/channels/ChannelInvitation.vue"
import { useUI } from "@/stores/ui"
const id = useUserinfo()
@@ -68,8 +66,8 @@ const isOwned = computed(() => {
return id.userinfo.idSet?.messaging === props.item?.account_id
})
const { showErrorSnackbar } = useUI()
const loading = ref(false)
const error = ref<string | null>(null)
watch(
() => props.item,
@@ -85,9 +83,8 @@ async function listMembers(id: number) {
loading.value = true
const res = await request("messaging", `/api/channels/${id}/members`)
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
error.value = null
members.value = await res.json()
}
loading.value = false
@@ -103,7 +100,7 @@ async function kickMember(item: any) {
})
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
await listMembers(props.item?.id)
}

View File

@@ -20,16 +20,16 @@
:alt="item.filename"
@click="openLightbox(item, idx)"
/>
<video v-if="item.type === 2" controls class="w-full content-visibility-auto">
<video v-else-if="item.type === 2" controls class="w-full content-visibility-auto">
<source :src="getUrl(item)" />
</video>
<div v-if="item.type === 3" class="w-[480px] py-12">
<div v-else-if="item.type === 3" class="py-5 px-2">
<div class="text-center">
<p class="mb-1">{{ getFileName(item) }}</p>
<audio controls :src="getUrl(item)" class="mx-auto"></audio>
<audio controls :src="getUrl(item)" class="mx-auto max-w-[85%]"></audio>
</div>
</div>
<div v-else class="w-[480px] py-12">
<div v-else class="py-5 px-2">
<div class="text-center">
<p>{{ getFileName(item) }}</p>
<a class="underline" target="_blank" :href="getUrl(item)">Download</a>

View File

@@ -1,6 +1,6 @@
<template>
<div class="text-xs text-center opacity-80">
<p>Copyright © {{ new Date().getFullYear() }} Solsynth</p>
<p>Powered by <a class="underline" href="#">Hydrogen.Identity</a></p>
<p>Powered by <a class="underline" href="#">Hydrogen</a></p>
</div>
</template>

View File

@@ -0,0 +1,28 @@
<template>
<v-snackbar v-model="ui.snackbar" v-bind="ui.snackbar">
<div v-html="ui.snackbar.content"></div>
<v-progress-linear v-if="ui.snackbar.loading" class="snackbar-progress" indeterminate />
</v-snackbar>
<v-snackbar v-model="ui.reconnection.messages">
<div>Reconnecting with messaging server...</div>
<v-progress-linear v-if="ui.snackbar.loading" class="snackbar-progress" indeterminate />
</v-snackbar>
<v-snackbar v-model="ui.reconnection.notifications">
<div>Reconnecting with notifications server...</div>
<v-progress-linear v-if="ui.snackbar.loading" class="snackbar-progress" indeterminate />
</v-snackbar>
</template>
<script setup lang="ts">
import { useUI } from "@/stores/ui"
const ui = useUI()
</script>
<style>
.snackbar-progress {
margin: 12px -16px -14px;
width: calc(100% + 64px);
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<v-list-item :title="otherside.nick">
<template #subtitle>@{{ otherside.name }}</template>
<template #prepend>
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card me-2"
size="small"
:image="othersidePicture"
/>
</template>
<template #append>
<v-btn
icon="mdi-check"
size="x-small"
color="success"
variant="text"
:disabled="!canApprove"
@click="emits('approve')"
/>
<v-btn
icon="mdi-close"
size="x-small"
color="error"
variant="text"
:disabled="!canDecline"
@click="emits('decline')"
/>
</template>
</v-list-item>
</template>
<script setup lang="ts">
import { computed } from "vue"
import { useUserinfo } from "@/stores/userinfo"
import { buildRequestUrl } from "@/scripts/request"
const id = useUserinfo()
const props = defineProps<{ item: any }>()
const emits = defineEmits(["approve", "decline"])
const canApprove = computed(() => {
return props.item.status === 2 ||
(props.item.status === 0 && props.item.related_id === id.userinfo.data?.id)
})
const canDecline = computed(() => {
return props.item.status !== 2
})
const otherside = computed(() => {
if (props.item.account_id === id.userinfo.data?.id) {
return props.item.related
} else {
return props.item.account
}
})
const othersidePicture = computed(() => otherside.value?.avatar ?
buildRequestUrl("identity", `/api/avatar/${otherside.value?.avatar}`) :
undefined
)
</script>

View File

@@ -0,0 +1,150 @@
<template>
<v-navigation-drawer
v-model="ui.drawer.open"
floating
color="grey-lighten-5"
width="320"
:permanent="isLargeScreen"
:rail="ui.drawer.mini"
:rail-width="58"
:order="0"
>
<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}`"
@click="expandDrawer"
>
<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"
@click="expandDrawer"
>
<channel-list />
<realm-list />
</v-list>
</div>
<!-- User info -->
<v-list
class="bg-grey-lighten-3"
:style="`margin-bottom: ${safeAreaBottom}`"
@click="expandDrawer"
>
<v-list-item :subtitle="username" :title="nickname">
<template #prepend>
<v-avatar icon="mdi-account-circle" :image="id.userinfo.data?.picture" />
</template>
<template #append>
<notification-list v-if="id.userinfo.isLoggedIn" />
<user-menu />
</template>
</v-list-item>
</v-list>
</div>
</v-navigation-drawer>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive } from "vue"
import { useUserinfo } from "@/stores/userinfo"
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"
}
})
function expandDrawer() {
ui.drawer.mini = false
}
onMounted(() => {
PullToRefresh.init({
mainElement: ".resources-list",
triggerElement: ".resources-list",
onRefresh() {
return Promise.all([
useRealms().list(),
useChannels().list()
])
}
})
})
</script>

View File

@@ -22,7 +22,7 @@ const editor = useEditor()
const props = defineProps<{ item: any }>()
const isOwned = computed(() => props.item?.author_id === id.userinfo.data.id)
const isOwned = computed(() => props.item?.author_id === id.userinfo.idSet.interactive)
function editPost() {
editor.related.edit_to = JSON.parse(JSON.stringify(props.item))

View File

@@ -22,13 +22,13 @@
<video v-else-if="item.type === 2" controls class="w-full content-visibility-auto">
<source :src="getUrl(item)" />
</video>
<div v-else-if="item.type === 3" class="w-[480px] py-12">
<div v-else-if="item.type === 3" class="py-5 px-2">
<div class="text-center">
<p class="mb-1">{{ getFileName(item) }}</p>
<audio controls :src="getUrl(item)" class="mx-auto"></audio>
<audio controls :src="getUrl(item)" class="mx-auto max-w-[85%]"></audio>
</div>
</div>
<div v-else class="w-[480px] py-12">
<div v-else class="py-5 px-2">
<div class="text-center">
<p>{{ getFileName(item) }}</p>
<a class="underline" target="_blank" :href="getUrl(item)">Download</a>

View File

@@ -1,12 +1,14 @@
<template>
<div class="flex gap-3">
<div>
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card"
:image="props.item?.author.avatar"
/>
<router-link :to="{ name: 'users.page', params: { alias: props.item?.author.name ?? 'ghost' } }">
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card"
:image="props.item?.author.avatar"
/>
</router-link>
</div>
<div class="flex-grow-1">

View File

@@ -3,7 +3,7 @@
<v-infinite-scroll :items="props.posts" :onLoad="props.loader">
<template v-for="(item, idx) in props.posts" :key="item.id">
<div class="mb-3 px-[8px]">
<v-card>
<v-card :variant="props.variant ?? 'elevated'">
<template #text>
<post-item brief :item="item" @update:item="(val) => updateItem(idx, val)" />
</template>
@@ -17,7 +17,7 @@
<script setup lang="ts">
import PostItem from "@/components/posts/PostItem.vue"
const props = defineProps<{ posts: any[]; loader: (opts: any) => Promise<any> }>()
const props = defineProps<{ variant?: any, posts: any[]; loader: (opts: any) => Promise<any> }>()
const emits = defineEmits(["update:posts"])
function updateItem(idx: number, data: any) {

View File

@@ -26,12 +26,6 @@
</v-list-item>
</v-list>
</v-menu>
<v-snackbar v-model="status.added" :timeout="3000">Your react has been added into post.</v-snackbar>
<v-snackbar v-model="status.removed" :timeout="3000">Your react has been removed from post.</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</div>
</template>
@@ -39,8 +33,10 @@
import { request } from "@/scripts/request"
import { getAtk, useUserinfo } from "@/stores/userinfo"
import { reactive, ref } from "vue"
import { useUI } from "@/stores/ui"
const id = useUserinfo()
const {showSnackbar, showErrorSnackbar} = useUI()
const emits = defineEmits(["update"])
const props = defineProps<{
@@ -62,9 +58,6 @@ function pickColor(): string {
return colors[randomIndex]
}
const status = reactive({ added: false, removed: false })
const error = ref<string | null>(null)
async function reactPost(symbol: string, attitude: number) {
const res = await request("interactive", `/api/p/${props.model}/${props.item?.id}/react`, {
method: "POST",
@@ -72,13 +65,13 @@ async function reactPost(symbol: string, attitude: number) {
body: JSON.stringify({ symbol, attitude })
})
if (res.status === 201) {
status.added = true
showSnackbar("Your react has been added onto the post.")
emits("update", symbol, 1)
} else if (res.status === 204) {
status.removed = true
showSnackbar("Your react has been removed from the post.")
emits("update", symbol, -1)
} else {
error.value = await res.text()
showErrorSnackbar(await res.text())
}
}
</script>

View File

@@ -109,14 +109,10 @@
<media ref="media" v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" />
<publish-area v-model:show="dialogs.area" v-model:value="data.realm_id" />
<v-snackbar v-model="success" :timeout="3000">Your article has been published.</v-snackbar>
<v-snackbar v-model="uploading" :timeout="-1">
Uploading your media, please stand by...
<v-progress-linear class="snackbar-progress" indeterminate />
</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
@@ -129,6 +125,7 @@ import { useRoute, useRouter } from "vue-router"
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
import Media from "@/components/publish/parts/PublishMedia.vue"
import PublishArea from "@/components/publish/parts/PublishArea.vue"
import { useUI } from "@/stores/ui"
const route = useRoute()
const realms = useRealms()
@@ -160,10 +157,9 @@ const currentRealm = computed(() => {
const router = useRouter()
const error = ref<string | null>(null)
const success = ref(false)
const reverting = ref(false)
const { showSnackbar, showErrorSnackbar } = useUI()
const loading = ref(false)
const reverting = ref(false)
const uploading = ref(false)
async function postArticle(evt: SubmitEvent) {
@@ -187,15 +183,15 @@ async function postArticle(evt: SubmitEvent) {
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
body: JSON.stringify(payload)
})
if (res.status === 200) {
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
const data = await res.json()
success.value = true
showSnackbar("Your article has been published.")
editor.show.article = false
resetEditor(form)
router.push({ name: "posts.details.articles", params: { alias: data.alias } })
} else {
error.value = await res.text()
await router.push({ name: "posts.details.articles", params: { alias: data.alias } })
}
loading.value = false
}

View File

@@ -19,11 +19,6 @@
</v-card-actions>
</v-form>
</v-card>
<v-snackbar v-model="success" :timeout="3000">Your comment has been published.</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
@@ -31,7 +26,9 @@ import { request } from "@/scripts/request"
import { useEditor } from "@/stores/editor"
import { getAtk } from "@/stores/userinfo"
import { computed, ref, watch } from "vue"
import { useUI } from "@/stores/ui"
const { showSnackbar, showErrorSnackbar } = useUI()
const editor = useEditor()
const target = computed<any>(() => editor.related.comment_to)
@@ -43,8 +40,6 @@ const postIdentifier = computed(() => {
}
})
const error = ref<string | null>(null)
const success = ref(false)
const loading = ref(false)
const data = ref<any>({
@@ -70,10 +65,10 @@ async function postComment(evt: SubmitEvent) {
})
if (res.status === 200) {
form.reset()
success.value = true
showSnackbar("Your comment has been published.")
editor.show.comment = false
} else {
error.value = await res.text()
showErrorSnackbar(await res.text())
}
loading.value = false
editor.done = true

View File

@@ -75,14 +75,10 @@
<media ref="media" v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" />
<publish-area v-model:show="dialogs.area" v-model:value="data.realm_id" />
<v-snackbar v-model="success" :timeout="3000">Your post has been published.</v-snackbar>
<v-snackbar v-model="uploading" :timeout="-1">
Uploading your media, please stand by...
<v-progress-linear class="snackbar-progress" indeterminate />
</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
@@ -94,6 +90,7 @@ import { useRoute, useRouter } from "vue-router"
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
import PublishArea from "@/components/publish/parts/PublishArea.vue"
import Media from "@/components/publish/parts/PublishMedia.vue"
import { useUI } from "@/stores/ui"
const route = useRoute()
const editor = useEditor()
@@ -111,8 +108,7 @@ const data = ref<any>({
attachments: []
})
const error = ref<string | null>(null)
const success = ref(false)
const { showSnackbar, showErrorSnackbar } = useUI()
const loading = ref(false)
const uploading = ref(false)
@@ -135,14 +131,15 @@ async function postMoment(evt: SubmitEvent) {
body: JSON.stringify(payload)
})
if (res.status === 200) {
resetEditor(form)
const data = await res.json()
success.value = true
editor.show.moment = false
resetEditor(form)
router.push({ name: "posts.details.moments", params: { alias: data.alias } })
showSnackbar("Your post has been published.")
await router.push({ name: "posts.details.moments", params: { alias: data.alias } })
} else {
error.value = await res.text()
showErrorSnackbar(await res.text())
}
loading.value = false
}

View File

@@ -12,11 +12,6 @@
</div>
</template>
</v-card>
<v-snackbar v-model="success" :timeout="3000">The post has been deleted.</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
@@ -24,11 +19,11 @@ import { request } from "@/scripts/request"
import { useEditor } from "@/stores/editor"
import { getAtk } from "@/stores/userinfo"
import { ref } from "vue"
import { useUI } from "@/stores/ui"
const editor = useEditor()
const error = ref<string | null>(null)
const success = ref(false)
const {showSnackbar, showErrorSnackbar} = useUI()
const loading = ref(false)
async function deletePost() {
@@ -41,9 +36,9 @@ async function deletePost() {
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
success.value = true
showSnackbar("The post has been deleted.")
editor.show.delete = false
editor.related.delete_to = null
}

View File

@@ -1,6 +1,5 @@
<template>
<v-dialog
eager
class="max-w-[540px]"
:model-value="props.show"
@update:model-value="(val) => emits('update:show', val)"

View File

@@ -12,11 +12,6 @@
</div>
</template>
</v-card>
<v-snackbar v-model="success" :timeout="3000">The realm has been deleted.</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
@@ -25,15 +20,15 @@ import { useRealms } from "@/stores/realms"
import { getAtk } from "@/stores/userinfo"
import { useRoute, useRouter } from "vue-router"
import { ref } from "vue"
import { useUI } from "@/stores/ui"
const { showSnackbar, showErrorSnackbar } = useUI()
const route = useRoute()
const router = useRouter()
const realms = useRealms()
const emits = defineEmits(["relist"])
const error = ref<string | null>(null)
const success = ref(false)
const loading = ref(false)
async function deleteRealm() {
@@ -46,9 +41,9 @@ async function deleteRealm() {
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
success.value = true
showSnackbar("The realm has been deleted.")
realms.show.delete = false
realms.related.delete_to = null
emits("relist")

View File

@@ -22,9 +22,6 @@
</v-card-actions>
</v-form>
</v-card>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
@@ -32,6 +29,7 @@ import { ref, watch } from "vue"
import { getAtk } from "@/stores/userinfo"
import { useRealms } from "@/stores/realms"
import { request } from "@/scripts/request"
import { useUI } from "@/stores/ui"
const emits = defineEmits(["relist"])
@@ -43,7 +41,7 @@ const realmTypeOptions = [
{ label: "Private Realm", value: 2 }
]
const error = ref<null | string>(null)
const { showErrorSnackbar } = useUI()
const loading = ref(false)
const data = ref({
@@ -67,7 +65,7 @@ async function submit(evt: SubmitEvent) {
body: JSON.stringify(payload)
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
emits("relist")
form.reset()

View File

@@ -2,11 +2,13 @@
<v-card prepend-icon="mdi-account-plus" title="Invite someone">
<v-form @submit.prevent="inviteMember">
<v-card-text>
<v-text-field
<v-autocomplete
label="Username"
variant="outlined"
density="comfortable"
hint="Require username not the nickname"
autocomplete="off"
hide-selected
:items="friends.available.map(x => getOtherside(x).name)"
v-model="targetName"
/>
</v-card-text>
@@ -23,11 +25,15 @@
<script setup lang="ts">
import { ref } from "vue"
import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
import { getAtk, useUserinfo } from "@/stores/userinfo"
import { useFriends } from "@/stores/friends"
const props = defineProps<{ item: any }>()
const emits = defineEmits(["close", "error", "relist"])
const id = useUserinfo()
const friends = useFriends()
const loading = ref(false)
const targetName = ref("")
@@ -51,4 +57,12 @@ async function inviteMember(evt: SubmitEvent) {
}
loading.value = false
}
function getOtherside(item: any) {
if (item.account_id === id.userinfo.data?.id) {
return item.related
} else {
return item.account
}
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<v-list-group value="realms">
<v-list-group class="realms-list" value="realms">
<template #activator="{ props }">
<v-list-item
v-bind="props"

View File

@@ -31,23 +31,20 @@
<div class="px-3">
<v-dialog class="max-w-[540px]">
<template #activator="{ props }">
<v-btn v-bind="props" block prepend-icon="mdi-account-plus" variant="plain"> Invite someone </v-btn>
<v-btn v-bind="props" block prepend-icon="mdi-account-plus" variant="plain"> Invite someone</v-btn>
</template>
<template #default="{ isActive }">
<realm-invitation
:item="props.item"
@relist="listMembers"
@error="(val) => (error = val)"
@error="(val) => (showErrorSnackbar(val))"
@close="isActive.value = false"
/>
</template>
</v-dialog>
</div>
</div>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</div>
</template>
@@ -57,6 +54,7 @@ import { request } from "@/scripts/request"
import { getAtk, useUserinfo } from "@/stores/userinfo"
import { computed } from "vue"
import RealmInvitation from "@/components/realms/RealmInvitation.vue"
import { useUI } from "@/stores/ui"
const id = useUserinfo()
@@ -65,11 +63,11 @@ const props = defineProps<{ item: any }>()
const members = ref<any[]>([])
const isOwned = computed(() => {
return id.userinfo.data?.id === props.item?.account_id
return id.userinfo.idSet?.interactive === props.item?.account_id
})
const { showErrorSnackbar } = useUI()
const loading = ref(false)
const error = ref<string | null>(null)
watch(
() => props.item,
@@ -85,9 +83,8 @@ async function listMembers(id: number) {
loading.value = true
const res = await request("interactive", `/api/realms/${id}/members`)
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
error.value = null
members.value = await res.json()
}
loading.value = false
@@ -103,7 +100,7 @@ async function kickMember(item: any) {
})
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
await listMembers(props.item?.id)
}

View File

@@ -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>
@@ -12,7 +12,8 @@
<v-list v-if="notify.notifications.length <= 0" class="w-[380px]" density="compact">
<v-list-item>
<v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for you.</v-alert>
<v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for you.
</v-alert>
</v-list-item>
</v-list>
@@ -31,34 +32,36 @@
</v-list-item>
</v-list>
</v-menu>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
import { computed, onMounted, onUnmounted, ref } from "vue";
import { useNotifications } from "@/stores/notifications";
import { computed, onMounted, onUnmounted, ref } from "vue"
import { useNotifications } from "@/stores/notifications"
import { useUI } from "@/stores/ui"
const notify = useNotifications()
const error = ref<string | null>(null)
const { showErrorSnackbar } = useUI()
const submitting = ref(false)
const loading = computed(() => notify.loading || submitting.value)
async function markAsRead(item: any, idx: number) {
if (item.is_realtime) {
notify.remove(idx)
return
}
submitting.value = true
const res = await request("identity", `/api/notifications/${item.id}/read`, {
method: "PUT",
headers: { Authorization: `Bearer ${getAtk()}` },
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
notify.remove(idx)
error.value = null
}
submitting.value = false
}

View File

@@ -0,0 +1,32 @@
<template>
<v-menu>
<template #activator="{ props }">
<v-btn icon="mdi-menu-up" size="small" variant="text" v-bind="props" />
</template>
<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 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" />
<v-list-item title="Sign out" prepend-icon="mdi-logout-variant" @click="signout" />
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import { signout as signoutAccount, useUserinfo } from "@/stores/userinfo"
const id = useUserinfo()
async function signout() {
signoutAccount().then(() => {
window.location.reload()
})
}
</script>

View File

@@ -4,17 +4,19 @@
<router-view />
<snackbar-provider />
<realm-tools />
<channel-tools />
</v-app>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue"
import { Capacitor } from "@capacitor/core"
import { onMounted } from "vue"
import { useUI } from "@/stores/ui"
import RealmTools from "@/components/realms/RealmTools.vue"
import ChannelTools from "@/components/chat/channels/ChannelTools.vue"
import SnackbarProvider from "@/components/common/SnackbarProvider.vue"
const ui = useUI()

View File

@@ -1,50 +1,94 @@
<template>
<v-app-bar :order="5" color="grey-lighten-3">
<v-app-bar :order="5" scroll-behavior="hide" color="grey-lighten-3">
<div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center max-w-full">
<v-app-bar-nav-icon icon="mdi-chat" :loading="loading" />
<v-app-bar-nav-icon icon="mdi-chat" :loading="loading" :to="{ name: 'explore' }" />
<h2 class="ml-2 text-lg font-500 overflow-hidden ws-nowrap text-clip">{{ channels.current?.name }}</h2>
<p class="ml-3 text-xs opacity-80 overflow-hidden ws-nowrap text-clip">{{ channels.current?.description }}</p>
<v-spacer />
</div>
<div v-if="channels.current">
<template v-if="channels.current" #append>
<v-btn
v-if="channels.call"
icon="mdi-phone-hangup"
size="small"
variant="text"
:loading="calling"
@click="endsCall"
/>
<v-btn
v-else
icon="mdi-phone-plus"
variant="text"
size="small"
:loading="calling"
@click="makeCall"
/>
<div class="me-5">
<channel-action :item="channels.current" />
</div>
</div>
</template>
</v-app-bar>
<router-view />
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
import { request } from "@/scripts/request"
import { useRoute } from "vue-router"
import { onMounted, ref, watch } from "vue"
import { onMounted, onUnmounted, ref, watch } from "vue"
import { useChannels } from "@/stores/channels"
import ChannelAction from "@/components/chat/channels/ChannelAction.vue"
import { useUI } from "@/stores/ui"
import { getAtk } from "@/stores/userinfo"
const { showErrorSnackbar } = useUI()
const ui = useUI()
const route = useRoute()
const channels = useChannels()
const error = ref<string | null>(null)
const loading = ref(false)
const calling = ref(false)
async function readMetadata() {
loading.value = true
const res = await request("messaging", `/api/channels/${route.params.channel}`)
if (res.status !== 200) {
error.value = await res.text()
showErrorSnackbar(await res.text())
} else {
error.value = null
channels.current = await res.json()
}
loading.value = false
}
async function makeCall() {
calling.value = true
const res = await request("messaging", `/api/channels/${route.params.channel}/calls`, {
method: "POST",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
}
calling.value = false
}
async function endsCall() {
calling.value = true
const res = await request("messaging", `/api/channels/${route.params.channel}/calls/ongoing`, {
method: "DELETE",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
}
calling.value = false
}
watch(
() => route.params.channel,
(val) => {
@@ -65,8 +109,17 @@ watch(() => channels.done, (val) => {
}
}, { immediate: true })
watch(
() => channels.current,
(val) => {
ui.appbar.show = !val
}
)
onMounted(() => {
channels.current = null
channels.messages = []
})
onUnmounted(() => ui.appbar.show = true)
</script>

View File

@@ -1,91 +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>
<v-list class="flex-grow-1" :opened="drawerMini ? [] : expanded" @update:opened="(val) => expanded = val">
<channel-list />
<v-divider class="border-opacity-75 my-2" />
<realm-list />
</v-list>
<!-- 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>
<v-menu v-if="id.userinfo.isLoggedIn">
<template #activator="{ props }">
<v-btn v-bind="props" icon="mdi-menu-down" size="small" variant="text" />
</template>
<v-list density="compact">
<v-list-item
title="Solarpass"
prepend-icon="mdi-passport-biometric"
target="_blank"
:href="passportUrl"
/>
</v-list>
</v-menu>
<v-btn v-else icon="mdi-login-variant" size="small" variant="text" :to="{ name: 'auth.sign-in' }" />
</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 && ui.appbar.show" height="64" color="primary" scroll-behavior="hide" :order="2">
<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>
@@ -95,53 +19,17 @@
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
import { useUserinfo } from "@/stores/userinfo"
import { useWellKnown } from "@/stores/wellKnown"
import { useMediaQuery } from "@vueuse/core"
import { useUI } from "@/stores/ui"
import RealmList from "@/components/realms/RealmList.vue"
import NotificationList from "@/components/NotificationList.vue"
import ChannelList from "@/components/chat/channels/ChannelList.vue"
import NavigationDrawer from "@/components/navigation/NavigationDrawer.vue"
const ui = useUI()
const expanded = ref<string[]>(["channels"])
const safeAreaTop = computed(() => {
return `${ui.safeArea.top}px`
})
const isLargeScreen = useMediaQuery("(min-width: 768px)")
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 meta = useWellKnown()
const passportUrl = computed(() => {
return meta.wellKnown?.components?.identity
})
meta.readWellKnown()
const drawerOpen = ref(true)
const drawerMini = ref(false)
useUserinfo().readProfiles()
useWellKnown().readWellKnown()
</script>

55
src/layouts/settings.vue Normal file
View File

@@ -0,0 +1,55 @@
<template>
<v-container class="wrapper pt-6 px-6">
<div class="content min-w-0">
<router-view />
</div>
<div class="aside-nav max-md:order-first">
<v-card prepend-icon="mdi-cog" title="Settings">
<v-list density="comfortable" class="overflow-auto">
<v-list-item title="Basis" prepend-icon="mdi-network" exact :to="{ name: 'settings' }" />
<v-divider class="border-[#000] my-2" />
<v-list-item title="Friends" prepend-icon="mdi-handshake" :to="{ name: 'settings.account.friends' }" />
<v-divider class="border-[#000] my-2" />
<v-list-item title="Personalize" prepend-icon="mdi-card-bulleted-outline" :to="{ name: 'settings.account.personalize' }" />
<v-list-item title="Personal Page" prepend-icon="mdi-sitemap" :to="{ name: 'settings.account.personal-page' }" />
<v-list-item title="Security" prepend-icon="mdi-security" :to="{ name: 'settings.account.security' }" />
<v-divider class="border-[#000] my-2" />
<v-list-item title="Solarpass" prepend-icon="mdi-passport-biometric" append-icon="mdi-launch" target="_blank" :href="passportUrl" />
</v-list>
</v-card>
</div>
</v-container>
</template>
<script setup lang="ts">
import { useWellKnown } from "@/stores/wellKnown"
import { computed } from "vue"
const meta = useWellKnown()
const passportUrl = computed(() => {
return meta.wellKnown?.components?.identity
})
</script>
<style scoped>
.wrapper {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 0.75rem;
}
@media (max-width: 768px) {
.wrapper {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -3,6 +3,8 @@ import "virtual:uno.css"
import "./assets/utils.css"
import "./assets/safe-area.css"
import "nprogress/nprogress.css"
import { createApp } from "vue"
import { createPinia } from "pinia"
@@ -17,9 +19,16 @@ import "@mdi/font/css/materialdesignicons.min.css"
import "@fontsource/roboto/latin.css"
import "@unocss/reset/tailwind.css"
import nprogress from "nprogress";
import index from "./index.vue"
import router from "./router"
nprogress.configure({showSpinner: false})
nprogress.start()
window.onload = () => nprogress.done()
const app = createApp(index)
app.use(

14
src/router/auth.ts Normal file
View File

@@ -0,0 +1,14 @@
export const authRouter = [
{
path: "sign-in",
name: "auth.sign-in",
component: () => import("@/views/auth/sign-in.vue"),
meta: { public: true, title: "Sign in" }
},
{
path: "sign-up",
name: "auth.sign-up",
component: () => import("@/views/auth/sign-up.vue"),
meta: { public: true, title: "Sign up" }
}
]

7
src/router/chat.ts Normal file
View File

@@ -0,0 +1,7 @@
export const chatRouter = [
{
path: "",
name: "chat.channel",
component: () => import("@/views/chat/page.vue"),
}
]

View File

@@ -1,6 +1,13 @@
import { createRouter, createWebHistory } from "vue-router"
import MasterLayout from "@/layouts/master.vue"
import nprogress from "nprogress";
import { authRouter } from "@/router/auth"
import { plazaRouter } from "@/router/plaza"
import { chatRouter } from "@/router/chat"
import { settingRouter } from "@/router/settings"
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
@@ -8,67 +15,44 @@ const router = createRouter({
path: "/",
component: MasterLayout,
children: [
{
path: "/u/:alias",
name: "users.page",
component: () => import("@/views/users/page.vue")
},
{
path: "/settings",
component: () => import("@/layouts/settings.vue"),
children: settingRouter,
},
{
path: "/",
component: () => import("@/layouts/plaza.vue"),
children: [
{
path: "/",
name: "explore",
component: () => import("@/views/explore.vue")
},
{
path: "/p/moments/:alias",
name: "posts.details.moments",
component: () => import("@/views/posts/moments.vue")
},
{
path: "/p/articles/:alias",
name: "posts.details.articles",
component: () => import("@/views/posts/articles.vue")
},
{
path: "/realms/:realmId",
name: "realms.page",
component: () => import("@/views/realms/page.vue")
}
]
children: plazaRouter,
},
{
path: "/chat/:channel",
component: () => import("@/layouts/chat.vue"),
children: [
{
path: "",
name: "chat.channel",
component: () => import("@/views/chat/page.vue"),
}
]
children: chatRouter,
},
{
path: "/auth",
children: [
{
path: "sign-in",
name: "auth.sign-in",
component: () => import("@/views/auth/sign-in.vue"),
meta: { public: true, title: "Sign in" }
},
{
path: "sign-up",
name: "auth.sign-up",
component: () => import("@/views/auth/sign-up.vue"),
meta: { public: true, title: "Sign up" }
}
]
children: authRouter,
}
]
}
]
})
router.beforeEach((_to, _from, next) => {
nprogress.start()
next()
})
router.afterEach(() => nprogress.done())
export default router

24
src/router/plaza.ts Normal file
View File

@@ -0,0 +1,24 @@
export const plazaRouter = [
{
path: "/",
name: "explore",
component: () => import("@/views/explore.vue")
},
{
path: "/p/moments/:alias",
name: "posts.details.moments",
component: () => import("@/views/posts/moments.vue")
},
{
path: "/p/articles/:alias",
name: "posts.details.articles",
component: () => import("@/views/posts/articles.vue")
},
{
path: "/realms/:realmId",
name: "realms.page",
component: () => import("@/views/realms/page.vue")
}
]

29
src/router/settings.ts Normal file
View File

@@ -0,0 +1,29 @@
export const settingRouter = [
{
path: "",
name: "settings",
component: () => import("@/views/settings.vue")
},
{
path: "account/friends",
name: "settings.account.friends",
component: () => import("@/views/users/me/friends.vue")
},
{
path: "account/personalize",
name: "settings.account.personalize",
component: () => import("@/views/users/me/personalize.vue")
},
{
path: "account/personal-page",
name: "settings.account.personal-page",
component: () => import("@/views/users/me/personal-page.vue")
},
{
path: "account/security",
name: "settings.account.security",
component: () => import("@/views/users/me/security.vue")
}
]

View File

@@ -4,7 +4,32 @@ import { Preferences } from "@capacitor/preferences"
const serviceMap: { [id: string]: string } = {
interactive: "https://co.solsynth.dev",
identity: "https://id.solsynth.dev",
messaging: "https://im.solsynth.dev",
messaging: "https://im.solsynth.dev"
}
export async function refreshToken() {
const res = await request("identity", "/api/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
refresh_token: await getRtk(),
grant_type: "refresh_token"
})
}, true)
if (res.status !== 200) {
const err = await res.text()
throw new Error(err)
} else {
const data = await res.json()
await Preferences.set({
key: "identity.access_token",
value: data["access_token"]
})
await Preferences.set({
key: "identity.refresh_token",
value: data["refresh_token"]
})
}
}
export async function request(service: string, input: string, init?: RequestInit, noRetry?: boolean) {
@@ -12,33 +37,13 @@ export async function request(service: string, input: string, init?: RequestInit
const res = await fetch(url, init)
if (res.status === 401 && !noRetry) {
const res = await request("identity", "/api/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
refresh_token: await getRtk(),
grant_type: "refresh_token"
})
}, true)
if (res.status !== 200) {
const err = await res.text()
throw new Error(err)
} else {
const data = await res.json()
await Preferences.set({
key: "identity.access_token",
value: data["access_token"]
})
await Preferences.set({
key: "identity.refresh_token",
value: data["refresh_token"]
})
}
await refreshToken()
console.info("[REQUEST] Auth context has been refreshed.")
return await request(service, input, Object.assign(init as any, {
headers: { Authorization: `Bearer ${await getAtk()}` }
}), true)
return await request(service, input, {
...init,
headers: { ...init?.headers, Authorization: `Bearer ${await getAtk()}` }
}, true)
}
return res

View File

@@ -1,11 +1,13 @@
import { defineStore } from "pinia"
import { reactive, ref, watch } from "vue"
import { checkLoggedIn, getAtk } from "@/stores/userinfo"
import { buildRequestUrl, request } from "@/scripts/request"
import { buildRequestUrl, refreshToken, request } from "@/scripts/request"
import { useRoute } from "vue-router"
import { useUI } from "@/stores/ui"
export const useChannels = defineStore("channels", () => {
let socket: WebSocket
let reconnectCount = 0
const done = ref(false)
@@ -33,13 +35,16 @@ export const useChannels = defineStore("channels", () => {
const available = ref<any[]>([])
const current = ref<any>(null)
const messages = ref<any[]>([])
const call = ref<any>(null)
const route = useRoute()
watch(
() => route.params.channel,
(val) => {
if (!val) {
call.value = null
messages.value = []
current.value = null
}
@@ -60,6 +65,24 @@ export const useChannels = defineStore("channels", () => {
}
}
const ui = useUI()
async function exchangeCallToken() {
if (!(await checkLoggedIn())) return
if (!current.value) return
const res = await request("messaging", `/api/channels/${current.value.alias}/calls/ongoing/token`, {
method: "POST",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
ui.showErrorSnackbar(`unable to exchange call token: ${await res.text()}`)
return null
} else {
return await res.json()
}
}
async function connect() {
if (!(await checkLoggedIn())) return
@@ -72,6 +95,22 @@ export const useChannels = defineStore("channels", () => {
})
socket.addEventListener("close", (event) => {
console.warn("[MESSAGING] The unified websocket is disconnected... ", event.reason, event.code)
const reconnect = () => {
reconnectCount = 0
refreshToken().then(() => {
connect().then(() => {
ui.reconnection.messages = false
reconnectCount++
})
})
}
ui.reconnection.messages = true
if (reconnectCount <= 3) {
reconnect()
} else {
setTimeout(() => reconnect(), 3000)
}
})
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data)
@@ -92,6 +131,13 @@ export const useChannels = defineStore("channels", () => {
return x.id !== payload.id
})
break
case "calls.new":
call.value = payload
break
case "calls.end":
call.value = null
break
}
}
})
@@ -101,5 +147,5 @@ export const useChannels = defineStore("channels", () => {
socket.close()
}
return { done, show, related, available, current, messages, list, connect, disconnect }
return { done, show, related, available, current, messages, call, list, exchangeCallToken, connect, disconnect }
})

23
src/stores/friends.ts Normal file
View File

@@ -0,0 +1,23 @@
import { reactive, ref } from "vue"
import { defineStore } from "pinia"
import { checkLoggedIn, getAtk } from "@/stores/userinfo"
import { request } from "@/scripts/request"
export const useFriends = defineStore("friends", () => {
const available = ref<any[]>([])
async function list() {
if (!(await checkLoggedIn())) return
const res = await request("identity", "/api/users/me/friends?status=1", {
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
throw new Error(await res.text())
} else {
available.value = await res.json()
}
}
return { available, list }
})

Some files were not shown because too many files have changed in this diff Show More