Compare commits
	
		
			41 Commits
		
	
	
		
			v2.0.0+2
			...
			0b24b7cc05
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0b24b7cc05 | |||
| 4e4bc3345d | |||
| 4a2ff8fce6 | |||
| 3a42c58013 | |||
| b6f50bbf53 | |||
| 21b2f1e555 | |||
| 7e01edffbe | |||
| 054e349e6b | |||
| 9bc387cb86 | |||
| 49bd6ea363 | |||
| 9ad11f4297 | |||
| 8af78a26ba | |||
| 031c3dee3b | |||
| 1f3f4a7370 | |||
| e36fc53df8 | |||
| 280a180d9e | |||
| 509d433959 | |||
| 169b5c0209 | |||
| 276c4f5dfe | |||
| 18d70382ff | |||
| 09154f1359 | |||
| fc1aef6eb7 | |||
| 5e2c6e6c3b | |||
| ff9f9b574b | |||
| 86b7fd85af | |||
| e0995b312c | |||
| e9e80bdeb5 | |||
| a8119e8366 | |||
| 43aad8c2d2 | |||
| c3bfb2069c | |||
| fbf45dab57 | |||
| 73b1e376a3 | |||
| 012a02751c | |||
| 634fedf17c | |||
| a5efec89f2 | |||
| 8bb9816cd0 | |||
| 05e8782557 | |||
| e986ff8c5f | |||
| c616214c3b | |||
| f552cdcf74 | |||
| d187ca0a88 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -29,3 +29,5 @@ coverage
 | 
			
		||||
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
*.lockb
 | 
			
		||||
 | 
			
		||||
*dist
 | 
			
		||||
@@ -9,6 +9,7 @@ android {
 | 
			
		||||
 | 
			
		||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation project(':capacitor-local-notifications')
 | 
			
		||||
    implementation project(':capacitor-preferences')
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,19 +3,18 @@
 | 
			
		||||
 | 
			
		||||
    <application
 | 
			
		||||
        android:allowBackup="true"
 | 
			
		||||
        android:label="Solian"
 | 
			
		||||
        android:icon="@mipmap/ic_launcher"
 | 
			
		||||
    android:label="@string/app_name"
 | 
			
		||||
        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:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
 | 
			
		||||
            android:exported="true"
 | 
			
		||||
            android:launchMode="singleTask"
 | 
			
		||||
      android:exported="true">
 | 
			
		||||
            android:theme="@style/AppTheme.NoActionBarLaunch">
 | 
			
		||||
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.MAIN" />
 | 
			
		||||
@@ -31,16 +30,19 @@
 | 
			
		||||
            android:grantUriPermissions="true">
 | 
			
		||||
            <meta-data
 | 
			
		||||
                android:name="android.support.FILE_PROVIDER_PATHS"
 | 
			
		||||
        android:resource="@xml/file_paths"></meta-data>
 | 
			
		||||
                android:resource="@xml/file_paths" />
 | 
			
		||||
        </provider>
 | 
			
		||||
    </application>
 | 
			
		||||
 | 
			
		||||
    <!-- Permissions -->
 | 
			
		||||
 | 
			
		||||
    <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"
 | 
			
		||||
    <uses-permission
 | 
			
		||||
        android:name="android.permission.READ_EXTERNAL_STORAGE"
 | 
			
		||||
        android:maxSdkVersion="32" />
 | 
			
		||||
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
 | 
			
		||||
    <uses-permission
 | 
			
		||||
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
 | 
			
		||||
        android:maxSdkVersion="29" />
 | 
			
		||||
</manifest>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/ic_launcher-playstore.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 31 KiB  | 
| 
		 Before Width: | Height: | Size: 7.5 KiB  | 
| 
		 Before Width: | Height: | Size: 3.9 KiB  | 
| 
		 Before Width: | Height: | Size: 9.0 KiB  | 
| 
		 Before Width: | Height: | Size: 14 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB  | 
| 
		 Before Width: | Height: | Size: 7.7 KiB  | 
| 
		 Before Width: | Height: | Size: 4.0 KiB  | 
| 
		 Before Width: | Height: | Size: 9.6 KiB  | 
| 
		 Before Width: | Height: | Size: 13 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB  | 
@@ -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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/icon_foreground.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 70 KiB  | 
| 
		 Before Width: | Height: | Size: 3.9 KiB  | 
@@ -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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 2.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.2 KiB  | 
| 
		 Before Width: | Height: | Size: 3.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
| 
		 Before Width: | Height: | Size: 3.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.5 KiB  | 
| 
		 Before Width: | Height: | Size: 845 B  | 
| 
		 Before Width: | Height: | Size: 1.6 KiB  | 
| 
		 Before Width: | Height: | Size: 1.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 820 B  | 
| 
		 Before Width: | Height: | Size: 2.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 Before Width: | Height: | Size: 2.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.6 KiB  | 
| 
		 Before Width: | Height: | Size: 4.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.7 KiB  | 
| 
		 Before Width: | Height: | Size: 4.9 KiB  | 
| 
		 After Width: | Height: | Size: 2.4 KiB  | 
| 
		 Before Width: | Height: | Size: 4.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.7 KiB  | 
| 
		 Before Width: | Height: | Size: 6.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.7 KiB  | 
| 
		 Before Width: | Height: | Size: 9.6 KiB  | 
| 
		 After Width: | Height: | Size: 3.7 KiB  | 
| 
		 Before Width: | Height: | Size: 6.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.9 KiB  | 
| 
		 Before Width: | Height: | Size: 10 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.8 KiB  | 
| 
		 Before Width: | Height: | Size: 15 KiB  | 
| 
		 After Width: | Height: | Size: 5.1 KiB  | 
| 
		 Before Width: | Height: | Size: 9.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 8.6 KiB  | 
@@ -17,6 +17,5 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
 | 
			
		||||
        <item name="android:background">@drawable/splash</item>
 | 
			
		||||
    </style>
 | 
			
		||||
</resources>
 | 
			
		||||
@@ -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')
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								index.html
									
									
									
									
									
								
							
							
						
						@@ -1,13 +1,14 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="UTF-8" />
 | 
			
		||||
    <link rel="icon" type="image/xml+svg" href="/favicon.png" />
 | 
			
		||||
  <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" />
 | 
			
		||||
  <title>Solian</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="module" src="/src/main.ts"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<div id="app"></div>
 | 
			
		||||
<script type="module" src="/src/main.ts"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								ios/App/App/App.entitlements
									
									
									
									
									
										Normal 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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -16,14 +16,17 @@
 | 
			
		||||
    "@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",
 | 
			
		||||
    "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 +38,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 +50,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
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 69 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 85 KiB  | 
@@ -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
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 75 KiB  | 
							
								
								
									
										2
									
								
								public/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
User-agent: *
 | 
			
		||||
Allow: /
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
							
								
								
									
										206
									
								
								src/components/chat/ChatEditor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,206 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-form class="flex-grow-1" ref="chat" @submit.prevent="sendMessage">
 | 
			
		||||
    <v-expand-transition>
 | 
			
		||||
      <v-alert
 | 
			
		||||
        v-show="channels.related?.messages?.reply_to"
 | 
			
		||||
        class="mb-3 text-sm"
 | 
			
		||||
        variant="tonal"
 | 
			
		||||
        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="info"
 | 
			
		||||
            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-3 text-sm"
 | 
			
		||||
        variant="tonal"
 | 
			
		||||
        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="info"
 | 
			
		||||
            variant="text"
 | 
			
		||||
            @click="channels.related.messages.edit_to = null"
 | 
			
		||||
          />
 | 
			
		||||
        </template>
 | 
			
		||||
      </v-alert>
 | 
			
		||||
    </v-expand-transition>
 | 
			
		||||
 | 
			
		||||
    <v-textarea
 | 
			
		||||
      auto-grow
 | 
			
		||||
      hide-details
 | 
			
		||||
      class="w-full"
 | 
			
		||||
      variant="outlined"
 | 
			
		||||
      density="compact"
 | 
			
		||||
      placeholder="Enter some messages..."
 | 
			
		||||
      :rows="1"
 | 
			
		||||
      :max-rows="6"
 | 
			
		||||
      :loading="loading"
 | 
			
		||||
      v-model="data.content"
 | 
			
		||||
      @keydown="onEditorKeydown"
 | 
			
		||||
      @paste="pasteMedia"
 | 
			
		||||
    >
 | 
			
		||||
      <template #append>
 | 
			
		||||
        <v-btn
 | 
			
		||||
          icon
 | 
			
		||||
          type="button"
 | 
			
		||||
          color="teal"
 | 
			
		||||
          size="small"
 | 
			
		||||
          variant="text"
 | 
			
		||||
          :disabled="loading"
 | 
			
		||||
          @click="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" :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) 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
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => channels.related.messages.edit_to,
 | 
			
		||||
  (val) => {
 | 
			
		||||
    if (val) {
 | 
			
		||||
      data.value = val
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
							
								
								
									
										20
									
								
								src/components/chat/ChatList.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										94
									
								
								src/components/chat/ChatMessage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,94 @@
 | 
			
		||||
<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" />
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
    <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="font-bold text-sm">{{ props.item?.sender.account.nick }}</div>
 | 
			
		||||
        <div>{{ props.item?.content }}</div>
 | 
			
		||||
 | 
			
		||||
        <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 MessageAttachment from "@/components/chat/renderer/MessageAttachment.vue"
 | 
			
		||||
 | 
			
		||||
const id = useUserinfo()
 | 
			
		||||
const channels = useChannels()
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ item: any }>()
 | 
			
		||||
 | 
			
		||||
const isOwned = computed(() => props.item?.sender?.id === id.userinfo.idSet.messaging)
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
</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>
 | 
			
		||||
							
								
								
									
										47
									
								
								src/components/chat/MessageDeletion.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										42
									
								
								src/components/chat/channels/ChannelAction.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,42 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-menu>
 | 
			
		||||
    <template #activator="{ props }">
 | 
			
		||||
      <v-btn v-bind="props" icon="mdi-cog" 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>
 | 
			
		||||
  </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>
 | 
			
		||||
							
								
								
									
										56
									
								
								src/components/chat/channels/ChannelDeletion.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										75
									
								
								src/components/chat/channels/ChannelEditor.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										54
									
								
								src/components/chat/channels/ChannelInvitation.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,54 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-card prepend-icon="mdi-account-plus" title="Invite someone">
 | 
			
		||||
    <v-form @submit.prevent="inviteMember">
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-text-field
 | 
			
		||||
          label="Username"
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          density="comfortable"
 | 
			
		||||
          hint="Require username not the nickname"
 | 
			
		||||
          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 } from "@/stores/userinfo"
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ item: any }>()
 | 
			
		||||
const emits = defineEmits(["close", "error", "relist"])
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										42
									
								
								src/components/chat/channels/ChannelList.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										121
									
								
								src/components/chat/channels/ChannelMembers.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										25
									
								
								src/components/chat/channels/ChannelTools.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										141
									
								
								src/components/chat/parts/ChatAttachments.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										102
									
								
								src/components/chat/renderer/MessageAttachment.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
							
								
								
									
										20
									
								
								src/components/common/SnackbarProvider.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
<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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useUI } from "@/stores/ui"
 | 
			
		||||
 | 
			
		||||
const ui = useUI()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.snackbar-progress {
 | 
			
		||||
  margin: 12px -16px -14px;
 | 
			
		||||
  width: calc(100% + 64px);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,14 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex gap-3">
 | 
			
		||||
    <div>
 | 
			
		||||
      <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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-dialog
 | 
			
		||||
    eager
 | 
			
		||||
    class="max-w-[540px]"
 | 
			
		||||
    :model-value="props.show"
 | 
			
		||||
    @update:model-value="(val) => emits('update:show', val)"
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,29 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-list density="comfortable">
 | 
			
		||||
    <v-list-subheader>
 | 
			
		||||
      Realms
 | 
			
		||||
      <v-badge color="warning" content="Alpha" inline />
 | 
			
		||||
    </v-list-subheader>
 | 
			
		||||
  <v-list-group class="realms-list" value="realms">
 | 
			
		||||
    <template #activator="{ props }">
 | 
			
		||||
      <v-list-item
 | 
			
		||||
        v-bind="props"
 | 
			
		||||
        prepend-icon="mdi-account-box-multiple"
 | 
			
		||||
        title="Realms"
 | 
			
		||||
      />
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
    <v-list-item
 | 
			
		||||
      v-for="item in realms.available"
 | 
			
		||||
      exact
 | 
			
		||||
      prepend-icon="mdi-account-multiple"
 | 
			
		||||
      append-icon="mdi-account-multiple"
 | 
			
		||||
      :to="{ name: 'realms.page', params: { realmId: item.id } }"
 | 
			
		||||
      :title="item.name"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <v-divider v-if="realms.available.length > 0" class="border-opacity-75 my-2" />
 | 
			
		||||
 | 
			
		||||
    <v-list-item
 | 
			
		||||
      prepend-icon="mdi-plus"
 | 
			
		||||
      append-icon="mdi-plus"
 | 
			
		||||
      title="Create a realm"
 | 
			
		||||
      variant="plain"
 | 
			
		||||
      :disabled="!id.userinfo.isLoggedIn"
 | 
			
		||||
      @click="createRealm"
 | 
			
		||||
    />
 | 
			
		||||
  </v-list>
 | 
			
		||||
  </v-list-group>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
  }
 | 
			
		||||
@@ -111,7 +108,7 @@ async function kickMember(item: any) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkKickable(item: any) {
 | 
			
		||||
  if (item.account?.id === id.userinfo.data?.id) return false
 | 
			
		||||
  if (item.account?.id === id.userinfo.idSet?.interactive) return false
 | 
			
		||||
  if (item.account?.id === props.item?.account_id) return false
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-bottom-sheet v-model="realms.show.editor">
 | 
			
		||||
  <v-bottom-sheet class="max-w-[480px]" v-model="realms.show.editor">
 | 
			
		||||
    <realm-editor @relist="realms.list" />
 | 
			
		||||
  </v-bottom-sheet>
 | 
			
		||||
  <v-bottom-sheet v-model="realms.show.delete">
 | 
			
		||||
  <v-bottom-sheet class="max-w-[480px]" v-model="realms.show.delete">
 | 
			
		||||
    <realm-deletion @relist="realms.list" />
 | 
			
		||||
  </v-bottom-sheet>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||