Compare commits

...

58 Commits

Author SHA1 Message Date
f8a838f5d7 🚑 Fix cannot click send button 2024-04-11 22:57:17 +08:00
ff55062850 💄 Better chatting ui
🐛 Fix multiple connections
2024-04-11 22:37:44 +08:00
c94dd8b761 ⬆️ Fix remote deps loading issue 2024-04-08 23:28:07 +08:00
06f8b9da85 💄 Better layout design 2024-04-08 23:14:09 +08:00
bbe6dbb2ca Support only attachments messages and markdown messages 2024-04-06 23:45:24 +08:00
3c02691511 🐛 Bug fixes of friends and voice chat 2024-04-06 23:36:10 +08:00
76367bbd25 Voice Chat yo! 2024-04-06 23:10:09 +08:00
8eb28f0115 Friend invitation auto complete 2024-04-06 15:10:06 +08:00
79cd1129fd 🐛 Fix wrong can approve detection 2024-04-06 03:10:58 +08:00
4c929a14fa 🐛 Fix wrong binding with approve and decline 2024-04-06 03:05:06 +08:00
2d3f8a8bd7 Friends 2024-04-06 02:08:57 +08:00
cbcb007517 Security page 2024-04-06 01:23:55 +08:00
f5603ad884 🐛 Fix button position mismatch 2024-04-05 22:07:40 +08:00
202b6c1a10 🐛 Fix infinite reconnect issue 2024-04-05 21:48:53 +08:00
c1f42ed4f7 Show time and id at messages 2024-04-05 21:07:55 +08:00
634347a958 🚑 Fix chat message issue 2024-04-05 16:04:25 +08:00
9039dfb34e 🚨 Fix tsc check 2024-04-05 13:30:57 +08:00
0b24b7cc05 Fix keybinding in chatting 2024-04-05 13:29:53 +08:00
4e4bc3345d Pull to refresh 2024-04-05 13:20:01 +08:00
4a2ff8fce6 🐛 Fix request bug 2024-04-05 13:03:29 +08:00
3a42c58013 🐛 Fix cannot read image in some browser 2024-04-05 11:45:54 +08:00
b6f50bbf53 🐛 Fix isOwned issue 2024-04-05 00:57:04 +08:00
21b2f1e555 🐛 Fix notifications 2024-04-04 23:23:41 +08:00
7e01edffbe Settings page 2024-04-04 22:36:04 +08:00
054e349e6b Image cropper and account settings 2024-04-04 22:28:15 +08:00
9bc387cb86 🎨 Deconstruct the snackbar 2024-04-04 18:35:48 +08:00
49bd6ea363 🐛 Websocket auto reconnecting 2024-04-02 23:31:24 +08:00
9ad11f4297 🐛 Fix some style issue 2024-04-02 23:29:28 +08:00
8af78a26ba 🚨 Fix typescript lint 2024-04-02 23:20:21 +08:00
031c3dee3b User personal page 2024-04-02 23:17:57 +08:00
1f3f4a7370 🐛 Bug fixes of chatting message misplace 2024-04-01 22:07:48 +08:00
e36fc53df8 🚀 Configure for android 2024-04-01 21:01:53 +08:00
280a180d9e Add progress bar 2024-04-01 20:29:04 +08:00
509d433959 Bug fixes 2024-04-01 20:21:01 +08:00
169b5c0209 Chat replying 2024-03-31 23:31:30 +08:00
276c4f5dfe 🍱 Modify cache glob 2024-03-31 22:35:39 +08:00
18d70382ff 💄 Optimize image browsing again 2024-03-31 22:17:30 +08:00
09154f1359 Messages update & deletion 2024-03-31 20:35:36 +08:00
fc1aef6eb7 🐛 Bug fixes 2024-03-31 18:38:37 +08:00
5e2c6e6c3b 🐛 Bug fixes 2024-03-31 18:26:24 +08:00
ff9f9b574b 🐛 Fix notify 2024-03-31 18:11:45 +08:00
86b7fd85af 🐛 Optimize icon 2024-03-31 18:10:52 +08:00
e0995b312c 🐛 Fix maskable icon 2024-03-31 18:05:22 +08:00
e9e80bdeb5 🐛 Fix icon 2024-03-31 18:00:09 +08:00
a8119e8366 🐛 Fix safe area issue 2024-03-31 17:54:27 +08:00
43aad8c2d2 Local Notifications 2024-03-31 17:49:31 +08:00
c3bfb2069c 🚚 Update request service map 2024-03-31 01:43:33 +08:00
fbf45dab57 Channels member manage 2024-03-31 01:18:25 +08:00
73b1e376a3 Channel manage 2024-03-31 01:06:06 +08:00
012a02751c Channel establish 2024-03-31 00:38:13 +08:00
634fedf17c Chat attachments 2024-03-31 00:07:04 +08:00
a5efec89f2 Chat message send and read history 2024-03-30 23:21:22 +08:00
8bb9816cd0 Basic chat layouts 2024-03-30 21:26:22 +08:00
05e8782557 💄 Optimized attachments view 2024-03-30 20:24:26 +08:00
e986ff8c5f 💄 Better speed dial 2024-03-30 20:08:39 +08:00
c616214c3b 💄 Better navbar 2024-03-30 19:24:19 +08:00
f552cdcf74 🐛 Bug fixes & optimization 2024-03-30 18:52:03 +08:00
d187ca0a88 Add full PWA support 2024-03-30 12:06:19 +08:00
138 changed files with 3751 additions and 842 deletions

2
.gitignore vendored
View File

@ -29,3 +29,5 @@ coverage
*.tsbuildinfo
*.lockb
*dist

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

@ -9,6 +9,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-local-notifications')
implementation project(':capacitor-preferences')
}

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

@ -2,5 +2,8 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-local-notifications'
project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android')
include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')

View File

@ -1,13 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/xml+svg" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Solian</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<head>
<meta charset="UTF-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>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -27,6 +27,7 @@
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
730477372BB91A4200A78988 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
@ -73,6 +74,7 @@
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
730477372BB91A4200A78988 /* App.entitlements */,
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
@ -345,6 +347,7 @@
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
@ -367,6 +370,7 @@
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:solsynth.dev</string>
</array>
</dict>
</plist>

View File

@ -20,8 +20,21 @@
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSApplicationCategoryType</key>
<string></string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Allow Solian use your camera so that you can take photo for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Allow Solian full access your photo library so that you can share photos more easily.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow Solian access your photo library so that you can share photos.</string>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -43,15 +56,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>LSApplicationCategoryType</key>
<string></string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Allow Solian full access your photo library so that you can share photos more easily.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow Solian access your photo library so that you can share photos.</string>
<key>NSCameraUsageDescription</key>
<string>Allow Solian use your camera so that you can take photo for your post.</string>
</dict>
</plist>

View File

@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
end

View File

@ -2,12 +2,15 @@ PODS:
- Capacitor (5.7.4):
- CapacitorCordova
- CapacitorCordova (5.7.4)
- CapacitorLocalNotifications (5.0.7):
- Capacitor
- CapacitorPreferences (5.0.7):
- Capacitor
DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
- "CapacitorLocalNotifications (from `../../node_modules/@capacitor/local-notifications`)"
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
EXTERNAL SOURCES:
@ -15,14 +18,17 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/ios"
CapacitorCordova:
:path: "../../node_modules/@capacitor/ios"
CapacitorLocalNotifications:
:path: "../../node_modules/@capacitor/local-notifications"
CapacitorPreferences:
:path: "../../node_modules/@capacitor/preferences"
SPEC CHECKSUMS:
Capacitor: 4fe9adf012caceb4c71ffea2f1f4d005cdcbeea7
CapacitorCordova: a6e87fccc0307dee7aec1560ec9398485f2b0ce7
CapacitorLocalNotifications: c58afadd159f6bc540ef9b3cbdbc82510a2bf112
CapacitorPreferences: 77ac427e98db83bace772455f8ba447430382c4c
PODFILE CHECKSUM: 769e120bf4dfe4ef1095b83775e36bafeeeb3cdd
PODFILE CHECKSUM: 19c3106e1cb0c8c0ae26243bfb70b974f8cfaaf5
COCOAPODS: 1.15.1

View File

@ -16,14 +16,19 @@
"@capacitor/android": "^5.7.4",
"@capacitor/core": "^5.7.4",
"@capacitor/ios": "^5.7.4",
"@capacitor/local-notifications": "^5.0.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",
"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"
@ -35,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",
@ -45,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",

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -1,46 +0,0 @@
{
"icons": [
{
"src": "../icons/icon-48.webp",
"type": "image/png",
"sizes": "48x48",
"purpose": "any maskable"
},
{
"src": "../icons/icon-72.webp",
"type": "image/png",
"sizes": "72x72",
"purpose": "any maskable"
},
{
"src": "../icons/icon-96.webp",
"type": "image/png",
"sizes": "96x96",
"purpose": "any maskable"
},
{
"src": "../icons/icon-128.webp",
"type": "image/png",
"sizes": "128x128",
"purpose": "any maskable"
},
{
"src": "../icons/icon-192.webp",
"type": "image/png",
"sizes": "192x192",
"purpose": "any maskable"
},
{
"src": "../icons/icon-256.webp",
"type": "image/png",
"sizes": "256x256",
"purpose": "any maskable"
},
{
"src": "../icons/icon-512.webp",
"type": "image/png",
"sizes": "512x512",
"purpose": "any maskable"
}
]
}

BIN
public/maskable-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Allow: /

View File

@ -2,7 +2,6 @@ html,
body,
#app,
.v-application {
overflow: auto !important;
font-family: "Roboto Sans", ui-sans-serif, system-ui, sans-serif;
}
@ -12,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,89 +0,0 @@
<template>
<v-menu eager :close-on-content-click="false">
<template #activator="{ props }">
<v-btn v-bind="props" stacked rounded="circle" size="small" variant="text" :loading="loading">
<v-badge v-if="pagination.total > 0" color="error" :content="pagination.total">
<v-icon icon="mdi-bell" />
</v-badge>
<v-icon v-else icon="mdi-bell" />
</v-btn>
</template>
<v-list v-if="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-list-item>
</v-list>
<v-list v-else class="w-[380px]" density="compact" lines="three">
<v-list-item v-for="item in notifications">
<template #title>{{ item.subject }}</template>
<template #subtitle>{{ item.content }}</template>
<template #append>
<v-btn icon="mdi-check" size="x-small" variant="text" :disabled="loading" @click="markAsRead(item)" />
</template>
<div class="flex text-xs gap-1">
<a v-for="link in item.links" class="mt-1 underline" target="_blank" :href="link.url">{{ link.label }}</a>
</div>
</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 { reactive, ref } from "vue"
const loading = ref(false)
const error = ref<string | null>(null)
const notifications = ref<any[]>([])
const pagination = reactive({ page: 1, pageSize: 25, total: 0 })
async function readNotifications() {
loading.value = true
const res = await request(
"identity",
"/api/notifications?" +
new URLSearchParams({
take: pagination.pageSize.toString(),
offset: ((pagination.page - 1) * pagination.pageSize).toString()
}),
{
headers: { Authorization: `Bearer ${await getAtk()}` }
}
)
if (res.status === 200) {
const data = await res.json()
notifications.value = data["data"]
pagination.total = data["count"]
}
loading.value = false
}
readNotifications()
async function markAsRead(item: any) {
loading.value = true
const res = await request("identity", `/api/notifications/${item.id}/read`, {
method: "PUT",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
error.value = await res.text()
} else {
await readNotifications()
error.value = null
}
loading.value = false
}
</script>

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

@ -0,0 +1,209 @@
<template>
<v-form ref="chat" @submit.prevent="sendMessage">
<v-expand-transition>
<v-alert
v-show="channels.related?.messages?.reply_to"
class="mb-2 text-sm"
variant="elevated"
density="compact"
type="info"
>
You are about replying a message #{{ channels.related?.messages?.reply_to?.id }}
<template #prepend>
<div class="h-[30px] flex items-center justify-center">
<v-icon icon="mdi-reply" size="small" />
</div>
</template>
<template #append>
<v-btn
icon="mdi-close"
size="x-small"
color="white"
variant="text"
@click="channels.related.messages.reply_to = null"
/>
</template>
</v-alert>
</v-expand-transition>
<v-expand-transition>
<v-alert
v-show="channels.related?.messages?.edit_to"
class="mb-2 text-sm"
variant="elevated"
density="compact"
type="info"
>
You are about editing a message #{{ channels.related?.messages?.edit_to?.id }}
<template #prepend>
<div class="h-[30px] flex items-center justify-center">
<v-icon icon="mdi-pencil" size="small" />
</div>
</template>
<template #append>
<v-btn
icon="mdi-close"
size="x-small"
color="white"
variant="text"
@click="channels.related.messages.edit_to = null"
/>
</template>
</v-alert>
</v-expand-transition>
<v-textarea
auto-grow
hide-details
class="w-full"
density="comfortable"
placeholder="Enter some messages..."
:rows="1"
:max-rows="6"
:loading="loading"
v-model="data.content"
@keydown="onEditorKeydown"
@paste="pasteMedia"
>
<template #append-inner>
<v-btn
icon
type="button"
color="teal"
size="small"
variant="text"
:disabled="loading"
@click.stop="dialogs.attachments = true"
>
<v-badge v-if="data.attachments.length > 0" :content="data.attachments.length">
<v-icon icon="mdi-paperclip" />
</v-badge>
<v-icon v-else icon="mdi-paperclip" />
</v-btn>
<v-btn type="submit" icon="mdi-send" size="small" variant="text" @click.stop :disabled="loading" />
</template>
</v-textarea>
<Attachments
ref="attachments"
v-model:show="dialogs.attachments"
v-model:uploading="uploading"
v-model:value="data.attachments"
/>
<v-snackbar v-model="uploading" :timeout="-1">
Uploading your media, please stand by...
<v-progress-linear class="snackbar-progress" indeterminate />
</v-snackbar>
</v-form>
</template>
<script setup lang="ts">
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"
const emits = defineEmits(["sent"])
const chat = ref<HTMLFormElement>()
const channels = useChannels()
const { showErrorSnackbar } = useUI()
const uploading = ref(false)
const loading = ref(false)
const attachments = ref<any>()
const dialogs = reactive({
attachments: false
})
const data = ref<any>({
content: "",
reply_id: null,
attachments: []
})
async function sendMessage() {
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}`
: `/api/channels/${channels.current.alias}/messages`
const method = channels.related.messages.edit_to ? "PUT" : "POST"
const payload = data.value
payload.reply_to = payload.reply_id
loading.value = true
const res = await request("messaging", url, {
method: method,
headers: { Authorization: `Bearer ${await getAtk()}`, "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
emits("sent")
resetEditor()
}
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
}
}
)
watch(
() => channels.related.messages.edit_to,
(val) => {
if (val) {
data.value = val
} else {
resetEditor()
}
}
)
function resetEditor() {
chat.value?.reset()
channels.related.messages.reply_to = null
channels.related.messages.edit_to = null
channels.related.messages.delete_to = null
data.value = {
content: "",
attachments: []
}
}
function pasteMedia(evt: ClipboardEvent) {
const files = evt.clipboardData?.files
if (files) {
Array.from(files).forEach((item) => {
attachments.value.upload(item)
})
}
}
</script>

View File

@ -0,0 +1,20 @@
<template>
<v-infinite-scroll
class="mt-[-16px] overflow-hidden"
:onLoad="props.loader"
>
<template v-for="item in props.messages" :key="item.id">
<chat-message class="px-6 py-2" :id="`m${item.id}`" :item="item" />
</template>
<template #empty>
<div class="flex-grow-1"></div>
</template>
</v-infinite-scroll>
</template>
<script setup lang="ts">
import ChatMessage from "@/components/chat/ChatMessage.vue"
const props = defineProps<{ loader: (opts: any) => Promise<any>, messages: any[] }>()
</script>

View File

@ -0,0 +1,125 @@
<template>
<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 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>
</div>
</a>
<div class="flex gap-2">
<div>
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card"
:image="props.item?.sender.account.avatar"
/>
</div>
<div class="flex-grow-1">
<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"
class="mt-1"
:attachments="props.item?.attachments"
/>
</div>
<div class="transition-opacity transition-300 message-action">
<v-card>
<div class="flex px-2 py-0.5">
<v-btn icon="mdi-reply" size="x-small" variant="text" @click="replyMessage" />
<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>
</div>
</div>
</template>
<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))
}
function editMessage() {
channels.related.messages.edit_to = JSON.parse(JSON.stringify(props.item))
}
function deleteMessage() {
channels.related.messages.delete_to = JSON.parse(JSON.stringify(props.item))
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>
.rounded-card {
border-radius: 8px;
}
.message-action {
position: absolute;
right: 8px;
top: -18px;
opacity: 0;
}
.message-item:hover {
background-color: rgba(0, 0, 0, .15);
}
.message-item:hover .message-action {
opacity: 100%;
}
</style>
<style>
.prose.prose-message p {
margin: 0 !important;
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<v-card title="Delete a message" class="min-h-[540px]" :loading="loading">
<template #text>
You are deleting a message
<b>#{{ channels.related?.messages?.delete_to?.id }}</b> <br />
This message will gone and never appear again. But the replies won't affected. Are you confirm?
</template>
<template #actions>
<div class="w-full flex justify-end">
<v-btn color="grey-darken-3" @click="channels.show.messages.delete = false">Not really</v-btn>
<v-btn color="error" :disabled="loading" @click="deleteMessage">Yes</v-btn>
</div>
</template>
</v-card>
</template>
<script setup lang="ts">
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 { showSnackbar, showErrorSnackbar } = useUI()
const loading = ref(false)
async function deleteMessage() {
const target = channels.related.messages.delete_to
const url = `/api/channels/${target.channel.alias}/messages/${target.id}`
loading.value = true
const res = await request("messaging", url, {
method: "DELETE",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
showSnackbar("The message has been deleted.")
channels.show.messages.delete = false
channels.related.messages.delete_to = null
}
loading.value = false
}
</script>

View File

@ -0,0 +1,42 @@
<template>
<v-menu>
<template #activator="{ props }">
<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-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>
<script setup lang="ts">
import { useUserinfo } from "@/stores/userinfo"
import { useChannels } from "@/stores/channels"
import { computed } from "vue"
const id = useUserinfo()
const channels = useChannels()
const props = defineProps<{ item: any }>()
const isOwned = computed(() => props.item?.account_id === id.userinfo.idSet?.messaging)
function editChannel() {
channels.related.edit_to = props.item
channels.show.editor = true
}
function manageChannel() {
channels.related.manage_to = props.item
channels.show.members = true
}
function deleteChannel() {
channels.related.delete_to = props.item
channels.show.delete = true
}
</script>

View File

@ -0,0 +1,56 @@
<template>
<v-card title="Delete a realm" class="min-h-[540px]" :loading="loading">
<template #text>
You are deleting a channel
<b>{{ channels.related.delete_to?.name }}</b> <br />
All messaging belonging to this channel will be deleted and never appear again. Are you confirm?
</template>
<template #actions>
<div class="w-full flex justify-end">
<v-btn color="grey-darken-3" @click="channels.show.delete = false">Not really</v-btn>
<v-btn color="error" :disabled="loading" @click="deleteChannel">Yes</v-btn>
</div>
</template>
</v-card>
</template>
<script setup lang="ts">
import { request } from "@/scripts/request"
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()
const channels = useChannels()
const emits = defineEmits(["relist"])
const { showSnackbar, showErrorSnackbar } = useUI()
const loading = ref(false)
async function deleteChannel() {
const target = channels.related.delete_to
const url = `/api/channels/${target.id}`
loading.value = true
const res = await request("messaging", url, {
method: "DELETE",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
showSnackbar("The channel has been deleted.")
channels.show.delete = false
channels.related.delete_to = null
emits("relist")
if (route.name?.toString()?.includes("channel")) {
await router.push({ name: "explore" })
}
}
loading.value = false
}
</script>

View File

@ -0,0 +1,75 @@
<template>
<v-card title="Establish a channel" prepend-icon="mdi-pound-box" class="min-h-[540px]" :loading="loading">
<v-form @submit.prevent="submit">
<v-card-text>
<v-text-field label="Alias" variant="outlined" density="comfortable" hint="Must be unique"
v-model="data.alias" />
<v-text-field label="Name" variant="outlined" density="comfortable" v-model="data.name" />
<v-textarea label="Description" variant="outlined" density="comfortable" v-model="data.description" />
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="reset" color="grey-darken-3" @click="channels.show.editor = false">Cancel</v-btn>
<v-btn type="submit" :disabled="loading">Save</v-btn>
</v-card-actions>
</v-form>
</v-card>
</template>
<script setup lang="ts">
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 {showErrorSnackbar} = useUI()
const loading = ref(false)
const data = ref({
alias: "",
name: "",
description: ""
})
async function submit(evt: SubmitEvent) {
const form = evt.target as HTMLFormElement
const payload = data.value
if (!payload.name) return
const url = channels.related.edit_to ? `/api/channels/${channels.related.edit_to?.id}` : "/api/channels"
const method = channels.related.edit_to ? "PUT" : "POST"
loading.value = true
const res = await request("messaging", url, {
method: method,
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
body: JSON.stringify(payload)
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
emits("relist")
form.reset()
channels.done = true
channels.show.editor = false
channels.related.edit_to = null
}
loading.value = false
}
watch(
channels.related,
(val) => {
if (val.edit_to) {
data.value = JSON.parse(JSON.stringify(val.edit_to))
}
},
{ immediate: true }
)
</script>

View File

@ -0,0 +1,68 @@
<template>
<v-card prepend-icon="mdi-account-plus" title="Invite someone">
<v-form @submit.prevent="inviteMember">
<v-card-text>
<v-autocomplete
label="Username"
variant="outlined"
density="comfortable"
autocomplete="off"
hide-selected
:items="friends.available.map(x => getOtherside(x).name)"
v-model="targetName"
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="reset" color="grey-darken-3" @click="emits('close')">Cancel</v-btn>
<v-btn type="submit" :disabled="loading">Invite</v-btn>
</v-card-actions>
</v-form>
</v-card>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { request } from "@/scripts/request"
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("")
async function inviteMember(evt: SubmitEvent) {
const form = evt.target as HTMLFormElement
loading.value = true
const res = await request("messaging", `/api/channels/${props.item?.id}/invite`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
body: JSON.stringify({
account_name: targetName.value
})
})
if (res.status !== 200) {
emits("error", await res.text())
} else {
form.reset()
emits("relist")
}
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

@ -0,0 +1,42 @@
<template>
<v-list-group class="channels-list" value="channels">
<template #activator="{ props }">
<v-list-item
v-bind="props"
prepend-icon="mdi-chat"
title="Channels"
/>
</template>
<v-list-item
v-for="item in channels.available"
exact
append-icon="mdi-pound-box"
:to="{ name: 'chat.channel', params: { channel: item.alias } }"
:title="item.name"
/>
<v-list-item
append-icon="mdi-plus"
title="Create a channel"
variant="plain"
:disabled="!id.userinfo.isLoggedIn"
@click="createChannel"
/>
</v-list-group>
</template>
<script setup lang="ts">
import { useUserinfo } from "@/stores/userinfo"
import { useChannels } from "@/stores/channels"
const id = useUserinfo()
const channels = useChannels()
function createChannel() {
channels.related.edit_to = null
channels.related.manage_to = null
channels.related.delete_to = null
channels.show.editor = true
}
</script>

View File

@ -0,0 +1,121 @@
<template>
<v-card title="Channel members" class="min-h-[540px]">
<v-list density="comfortable" lines="one">
<v-list-item v-for="item in members" :title="item.account.nick">
<template #subtitle>@{{ item.account.name }}</template>
<template #prepend>
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card me-2"
size="small"
:image="item?.account.avatar"
/>
</template>
<template #append>
<v-btn
icon="mdi-account-remove"
size="x-small"
color="error"
variant="text"
:disabled="!checkKickable(item)"
@click="kickMember(item)"
/>
</template>
</v-list-item>
</v-list>
<div v-if="isOwned">
<v-divider class="mt-2 mb-3 border-opacity-50 mx-[-1rem]" />
<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>
</template>
<template #default="{ isActive }">
<channel-invitation
:item="props.item"
@relist="listMembers"
@error="(val) => (showErrorSnackbar(val))"
@close="isActive.value = false"
/>
</template>
</v-dialog>
</div>
</div>
</v-card>
</template>
<script setup lang="ts">
import { ref, watch } from "vue"
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()
const props = defineProps<{ item: any }>()
const members = ref<any[]>([])
const isOwned = computed(() => {
return id.userinfo.idSet?.messaging === props.item?.account_id
})
const { showErrorSnackbar } = useUI()
const loading = ref(false)
watch(
() => props.item,
(val) => {
if (val?.id) {
listMembers(val.id)
}
},
{ deep: true, immediate: true }
)
async function listMembers(id: number) {
loading.value = true
const res = await request("messaging", `/api/channels/${id}/members`)
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
members.value = await res.json()
}
loading.value = false
}
async function kickMember(item: any) {
loading.value = true
const res = await request("messaging", `/api/channels/${props.item?.id}/kick`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
body: JSON.stringify({
account_name: item.account.name
})
})
if (res.status !== 200) {
showErrorSnackbar(await res.text())
} else {
await listMembers(props.item?.id)
}
loading.value = false
}
function checkKickable(item: any) {
if (item.account?.id === id.userinfo.idSet?.messaging) return false
if (item.account?.id === props.item?.account_id) return false
return true
}
</script>
<style>
.rounded-card {
border-radius: 8px;
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<v-bottom-sheet class="max-w-[480px]" v-model="channels.show.editor">
<channel-editor @relist="channels.list" />
</v-bottom-sheet>
<v-bottom-sheet class="max-w-[480px]" v-model="channels.show.members">
<channel-members :item="channels.related.manage_to" @relist="channels.list" />
</v-bottom-sheet>
<v-bottom-sheet class="max-w-[480px]" v-model="channels.show.delete">
<channel-deletion @relist="channels.list" />
</v-bottom-sheet>
<v-bottom-sheet class="max-w-[480px]" v-model="channels.show.messages.delete">
<message-deletion />
</v-bottom-sheet>
</template>
<script setup lang="ts">
import { useChannels } from "@/stores/channels"
import ChannelEditor from "@/components/chat/channels/ChannelEditor.vue"
import ChannelMembers from "@/components/chat/channels/ChannelMembers.vue"
import ChannelDeletion from "@/components/chat/channels/ChannelDeletion.vue"
import MessageDeletion from "@/components/chat/MessageDeletion.vue"
const channels = useChannels()
</script>

View File

@ -0,0 +1,141 @@
<template>
<v-dialog
eager
class="max-w-[540px]"
:model-value="props.show"
@update:model-value="(val) => emits('update:show', val)"
>
<v-card title="Attachments">
<template #text>
<v-file-input
prepend-icon=""
append-icon="mdi-upload"
variant="solo-filled"
label="File Picker"
v-model="picked"
:loading="props.uploading"
@click:append="upload()"
/>
<h2 class="px-2 mb-1">Media list</h2>
<v-card variant="tonal">
<v-list>
<v-list-item v-for="(item, idx) in props.value" :title="getFileName(item)">
<template #subtitle> {{ getFileType(item) }} · {{ formatBytes(item.filesize) }}</template>
<template #append>
<v-btn icon="mdi-delete" size="small" variant="text" color="error" @click="dispose(idx)" />
</template>
</v-list-item>
</v-list>
</v-card>
</template>
<template #actions>
<v-btn class="ms-auto" text="Ok" @click="emits('update:show', false)"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
import { ref } from "vue"
const props = defineProps<{ show: boolean; uploading: boolean; value: any[] }>()
const emits = defineEmits(["update:show", "update:uploading", "update:value"])
const picked = ref<any[]>([])
const error = ref<string | null>(null)
async function upload(file?: any) {
if (props.uploading) return
const data = new FormData()
if (!file) {
file = picked.value[0]
}
data.set("attachment", file)
data.set("hashcode", await calculateHashCode(file))
emits("update:uploading", true)
const res = await request("messaging", "/api/attachments", {
method: "POST",
headers: { Authorization: `Bearer ${await getAtk()}` },
body: data
})
let meta: any
if (res.status !== 200) {
error.value = await res.text()
} else {
meta = await res.json()
emits("update:value", props.value.concat([meta.info]))
picked.value = []
}
emits("update:uploading", false)
return meta
}
async function dispose(idx: number) {
const media = JSON.parse(JSON.stringify(props.value))
const item = media.splice(idx)[0]
emits("update:value", media)
const res = await request("messaging", `/api/attachments/${item.id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${await getAtk()}` }
})
if (res.status !== 200) {
error.value = await res.text()
}
}
defineExpose({ upload, dispose })
async function calculateHashCode(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = async () => {
const buffer = reader.result as ArrayBuffer
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("")
resolve(hashHex)
}
reader.onerror = () => {
reject(reader.error)
}
reader.readAsArrayBuffer(file)
})
}
function getFileName(item: any) {
return item.filename.replace(/\.[^/.]+$/, "")
}
function getFileType(item: any) {
switch (item.type) {
case 1:
return "Photo"
case 2:
return "Video"
case 3:
return "Audio"
default:
return "Others"
}
}
function formatBytes(bytes: number, decimals = 2) {
if (!+bytes) return "0 Bytes"
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
</script>

View File

@ -0,0 +1,102 @@
<template>
<v-chip size="small" variant="tonal" prepend-icon="mdi-paperclip" v-if="props.overview">
Attached {{ props.attachments.length }} attachment(s)
</v-chip>
<v-card variant="outlined" class="w-fit max-h-[540px]">
<v-carousel
hide-delimiter-background
height="100%"
:hide-delimiters="props.attachments.length <= 1"
:show-arrows="false"
>
<v-carousel-item v-for="(item, idx) in attachments">
<img
v-if="item.type === 1"
loading="lazy"
decoding="async"
class="cursor-zoom-in content-visibility-auto max-h-[540px] object-cover object-c"
:src="getUrl(item)"
:alt="item.filename"
@click="openLightbox(item, idx)"
/>
<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="py-5 px-2">
<div class="text-center">
<p class="mb-1">{{ getFileName(item) }}</p>
<audio controls :src="getUrl(item)" class="mx-auto max-w-[85%]"></audio>
</div>
</div>
<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>
</div>
</div>
</v-carousel-item>
</v-carousel>
<vue-easy-lightbox
teleport="#app"
:visible="lightbox"
:imgs="props.attachments.map((x) => getUrl(x))"
v-model:index="currentIndex"
@hide="lightbox = false"
>
<template v-slot:close-btn="{ close }">
<v-btn
class="fixed left-2 top-2"
icon="mdi-close"
variant="text"
color="white"
:style="`margin-top: ${safeAreaTop}`"
@click="close"
/>
</template>
</vue-easy-lightbox>
</v-card>
</template>
<script setup lang="ts">
import { buildRequestUrl } from "@/scripts/request"
import { computed, ref } from "vue"
import { useUI } from "@/stores/ui"
import VueEasyLightbox from "vue-easy-lightbox"
const props = defineProps<{ attachments: any[]; overview?: boolean }>()
const ui = useUI()
const lightbox = ref(false)
const current = ref<any>(null)
const currentIndex = ref(0)
const safeAreaTop = computed(() => {
return `${ui.safeArea.top}px`
})
function getFileName(item: any) {
return item.filename.replace(/\.[^/.]+$/, "")
}
function getUrl(item: any) {
return item.external_url
? item.external_url
: buildRequestUrl("messaging", `/api/attachments/o/${item.file_id}`)
}
function openLightbox(item: any, idx: number) {
current.value = item
currentIndex.value = idx
lightbox.value = true
}
</script>
<style>
.vel-model {
z-index: 10;
}
</style>

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

@ -3,21 +3,47 @@
Attached {{ props.attachments.length }} attachment(s)
</v-chip>
<v-card v-else variant="outlined" class="max-w-[540px] max-h-[720px]">
<v-carousel hide-delimiter-background height="100%" :show-arrows="false">
<v-carousel-item v-for="item in attachments">
<img v-if="item.type === 1" :src="getUrl(item)" :alt="item.filename" class="cursor-zoom-in"
@click="openLightbox" />
<video v-if="item.type === 2" controls class="w-full">
<v-card v-else variant="outlined" class="w-fit max-h-[540px]">
<v-carousel
hide-delimiter-background
height="100%"
:hide-delimiters="props.attachments.length <= 1"
:show-arrows="false"
>
<v-carousel-item v-for="(item, idx) in attachments">
<img
v-if="item.type === 1"
decoding="async"
class="cursor-zoom-in max-h-[540px] object-cover object-c"
:src="getUrl(item)"
:alt="getFileName(item)"
@click="openLightbox(item, idx)"
/>
<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-full px-7 py-12">
<audio controls :src="getUrl(item)" class="mx-auto"></audio>
<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 max-w-[85%]"></audio>
</div>
</div>
<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>
</div>
</div>
</v-carousel-item>
</v-carousel>
<vue-easy-lightbox teleport="#app" :visible="lightbox" :imgs="[getUrl(current)]" @hide="lightbox = false">
<vue-easy-lightbox
teleport="#app"
:visible="lightbox"
:imgs="props.attachments.map((x) => getUrl(x))"
v-model:index="currentIndex"
@hide="lightbox = false"
>
<template v-slot:close-btn="{ close }">
<v-btn
class="fixed left-2 top-2"
@ -43,23 +69,28 @@ const props = defineProps<{ attachments: any[]; overview?: boolean }>()
const ui = useUI()
const lightbox = ref(false)
const focus = ref(0)
const current = computed(() => props.attachments[focus.value])
const canLightbox = computed(() => current.value.type === 1)
const current = ref<any>(null)
const currentIndex = ref(0)
const safeAreaTop = computed(() => {
return `${ui.safeArea.top}px`
})
function getUrl(item: any) {
return item.external_url ? item.external_url : buildRequestUrl("interactive", `/api/attachments/o/${item.file_id}`)
function getFileName(item: any) {
return item.filename.replace(/\.[^/.]+$/, "")
}
function openLightbox() {
if (canLightbox.value) {
lightbox.value = true
}
function getUrl(item: any) {
return item.external_url
? item.external_url
: buildRequestUrl("interactive", `/api/attachments/o/${item.file_id}`)
}
function openLightbox(item: any, idx: number) {
current.value = item
currentIndex.value = idx
lightbox.value = true
}
</script>

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

@ -1,9 +1,9 @@
<template>
<div class="post-list">
<div class="post-list mx-[-8px]">
<v-infinite-scroll :items="props.posts" :onLoad="props.loader">
<template v-for="(item, idx) in props.posts" :key="item">
<div class="mb-3 px-1">
<v-card>
<template v-for="(item, idx) in props.posts" :key="item.id">
<div class="mb-3 px-[8px]">
<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">
@ -127,8 +123,9 @@ import { useRealms } from "@/stores/realms"
import { computed, reactive, ref, watch } from "vue"
import { useRoute, useRouter } from "vue-router"
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
import Media from "@/components/publish/parts/Media.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
}
@ -262,10 +258,7 @@ watch(
}
.snackbar-progress {
margin-left: -16px;
margin-right: -16px;
margin-bottom: -14px;
margin-top: 12px;
margin: 12px -16px -14px;
width: calc(100% + 64px);
}
</style>

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">
@ -93,7 +89,8 @@ import { reactive, ref, watch } from "vue"
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/Media.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
}
@ -188,10 +185,7 @@ watch(
<style>
.snackbar-progress {
margin-left: -16px;
margin-right: -16px;
margin-bottom: -14px;
margin-top: 12px;
margin: 12px -16px -14px;
width: calc(100% + 64px);
}
</style>

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

@ -22,7 +22,7 @@ const realms = useRealms()
const props = defineProps<{ item: any }>()
const isOwned = computed(() => props.item?.account_id === id.userinfo.data.id)
const isOwned = computed(() => props.item?.account_id === id.userinfo.idSet?.interactive)
function editRealm() {
realms.related.edit_to = props.item

View File

@ -3,20 +3,15 @@
<template #text>
You are deleting a realm
<b>{{ realms.related.delete_to?.name }}</b> <br />
All posts belonging to this domain will be deleted and never appear again. Are you confirm?
All posts belonging to this realm will be deleted and never appear again. Are you confirm?
</template>
<template #actions>
<div class="w-full flex justify-end">
<v-btn color="grey-darken-3" @click="realms.show.delete = false">Not really</v-btn>
<v-btn color="error" :disabled="loading" @click="deletePost">Yes</v-btn>
<v-btn color="error" :disabled="loading" @click="deleteRealm">Yes</v-btn>
</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,18 +20,18 @@ 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 deletePost() {
async function deleteRealm() {
const target = realms.related.delete_to
const url = `/api/realms/${target.id}`
@ -46,14 +41,14 @@ 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 realm has been deleted.")
realms.show.delete = false
realms.related.delete_to = null
emits("relist")
if (route.name?.toString()?.startsWith("realm")) {
router.push({ name: "explore" })
if (route.name?.toString()?.includes("realm")) {
await router.push({ name: "explore" })
}
}
loading.value = false

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()

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