✨ Local Notifications
This commit is contained in:
		| @@ -9,6 +9,7 @@ android { | ||||
|  | ||||
| apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" | ||||
| dependencies { | ||||
|     implementation project(':capacitor-local-notifications') | ||||
|     implementation project(':capacitor-preferences') | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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') | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										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,6 +16,7 @@ | ||||
|     "@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", | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <template> | ||||
|   <v-menu eager :close-on-content-click="false"> | ||||
|     <template #activator="{ props }"> | ||||
|       <v-btn v-bind="props" icon rounded="circle" size="small" variant="text" :loading="loading"> | ||||
|         <v-badge v-if="pagination.total > 0" color="error" :content="pagination.total"> | ||||
|       <v-btn v-bind="props" icon size="small" variant="text" :loading="loading"> | ||||
|         <v-badge v-if="notify.total > 0" color="error" :content="notify.total"> | ||||
|           <v-icon icon="mdi-bell" /> | ||||
|         </v-badge> | ||||
|  | ||||
| @@ -10,20 +10,19 @@ | ||||
|       </v-btn> | ||||
|     </template> | ||||
|  | ||||
|     <v-list v-if="notifications.length <= 0" class="w-[380px]" density="compact"> | ||||
|     <v-list v-if="notify.notifications.length <= 0" class="w-[380px]" density="compact"> | ||||
|       <v-list-item> | ||||
|         <v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for | ||||
|           you.</v-alert> | ||||
|         <v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for you.</v-alert> | ||||
|       </v-list-item> | ||||
|     </v-list> | ||||
|  | ||||
|     <v-list v-else class="w-[380px]" density="compact" lines="three"> | ||||
|       <v-list-item v-for="item in notifications"> | ||||
|       <v-list-item v-for="(item, idx) in notify.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)" /> | ||||
|           <v-btn icon="mdi-check" size="x-small" variant="text" :disabled="loading" @click="markAsRead(item, idx)" /> | ||||
|         </template> | ||||
|  | ||||
|         <div class="flex text-xs gap-1"> | ||||
| @@ -40,50 +39,32 @@ | ||||
| <script setup lang="ts"> | ||||
| import { request } from "@/scripts/request" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
| import { reactive, ref } from "vue" | ||||
| import { computed, onMounted, onUnmounted, ref } from "vue"; | ||||
| import { useNotifications } from "@/stores/notifications"; | ||||
|  | ||||
| const loading = ref(false) | ||||
| const notify = useNotifications() | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
| const submitting = ref(false) | ||||
| const loading = computed(() => notify.loading || submitting.value) | ||||
|  | ||||
| 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`, { | ||||
| async function markAsRead(item: any, idx: number) { | ||||
|   submitting.value = true | ||||
|   const res = await request(`/api/notifications/${item.id}/read`, { | ||||
|     method: "PUT", | ||||
|     headers: { Authorization: `Bearer ${await getAtk()}` } | ||||
|     headers: { Authorization: `Bearer ${getAtk()}` }, | ||||
|   }) | ||||
|   if (res.status !== 200) { | ||||
|     error.value = await res.text() | ||||
|   } else { | ||||
|     await readNotifications() | ||||
|     notify.remove(idx) | ||||
|     error.value = null | ||||
|   } | ||||
|   loading.value = false | ||||
|   submitting.value = false | ||||
| } | ||||
|  | ||||
| notify.list() | ||||
|  | ||||
| onMounted(() => notify.connect()) | ||||
| onUnmounted(() => notify.disconnect()) | ||||
| </script> | ||||
|   | ||||
| @@ -14,7 +14,6 @@ | ||||
|         <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 w-full h-full object-contain" | ||||
|             :src="getUrl(item)" | ||||
|   | ||||
							
								
								
									
										81
									
								
								src/stores/notifications.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/stores/notifications.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| import { defineStore } from "pinia" | ||||
| import { ref } from "vue" | ||||
| import { checkLoggedIn, getAtk } from "@/stores/userinfo" | ||||
| import { buildRequestUrl, request } from "@/scripts/request" | ||||
| import { LocalNotifications } from "@capacitor/local-notifications" | ||||
| import { Capacitor } from "@capacitor/core" | ||||
|  | ||||
| export const useNotifications = defineStore("notifications", () => { | ||||
|   let socket: WebSocket | ||||
|  | ||||
|   const loading = ref(false) | ||||
|  | ||||
|   const notifications = ref<any[]>([]) | ||||
|   const total = ref(0) | ||||
|  | ||||
|   async function list() { | ||||
|     loading.value = true | ||||
|     const res = await request( | ||||
|       "identity", | ||||
|       "/api/notifications?" + | ||||
|       new URLSearchParams({ | ||||
|         take: (25).toString(), | ||||
|         offset: (0).toString() | ||||
|       }), | ||||
|       { | ||||
|         headers: { Authorization: `Bearer ${await getAtk()}` } | ||||
|       } | ||||
|     ) | ||||
|     if (res.status === 200) { | ||||
|       const data = await res.json() | ||||
|       notifications.value = data["data"] | ||||
|       total.value = data["count"] | ||||
|     } | ||||
|     loading.value = false | ||||
|   } | ||||
|  | ||||
|   function remove(idx: number) { | ||||
|     notifications.value.splice(idx, 1) | ||||
|     total.value-- | ||||
|   } | ||||
|  | ||||
|   async function connect() { | ||||
|     if (!(await checkLoggedIn())) return | ||||
|  | ||||
|     const uri = buildRequestUrl("identity", "/api/notifications/listen").replace("http", "ws") | ||||
|  | ||||
|     socket = new WebSocket(uri + `?tk=${await getAtk() as string}`) | ||||
|  | ||||
|     socket.addEventListener("open", (event) => { | ||||
|       console.log("[NOTIFICATIONS] The listen websocket has been established... ", event.type) | ||||
|     }) | ||||
|     socket.addEventListener("close", (event) => { | ||||
|       console.warn("[NOTIFICATIONS] The listen websocket is disconnected... ", event.reason, event.code) | ||||
|     }) | ||||
|     socket.addEventListener("message", (event) => { | ||||
|       const data = JSON.parse(event.data) | ||||
|       notifications.value.push(data) | ||||
|       total.value++ | ||||
|  | ||||
|       if (Capacitor.getPlatform() === "web") { | ||||
|         new Notification(data["id"], { | ||||
|           body: data["subject"] | ||||
|         }) | ||||
|       } else { | ||||
|         LocalNotifications.schedule({ | ||||
|           notifications: [ | ||||
|             { id: data["id"], title: data["subject"], body: data["subject"] } | ||||
|           ] | ||||
|         }).then((res) => console.log(res)) | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     await Notification.requestPermission() | ||||
|   } | ||||
|  | ||||
|   function disconnect() { | ||||
|     socket.close() | ||||
|   } | ||||
|  | ||||
|   return { loading, notifications, total, list, remove, connect, disconnect } | ||||
| }) | ||||
| @@ -4,7 +4,7 @@ | ||||
|       <post-list v-model:posts="posts" :loader="readMore" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="aside md:sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px] max-md:order-first"> | ||||
|     <div class="aside w-full h-full md:min-w-[320px] md:max-w-[320px] max-md:order-first"> | ||||
|       <v-card title="Categories"> | ||||
|         <v-list density="compact"> | ||||
|           <v-list-item title="All" prepend-icon="mdi-apps" active></v-list-item> | ||||
| @@ -16,8 +16,8 @@ | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import PostList from "@/components/posts/PostList.vue" | ||||
| import { reactive, ref } from "vue" | ||||
| import { request } from "@/scripts/request" | ||||
| import { reactive, ref } from "vue" | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
| const pagination = reactive({ page: 1, pageSize: 10, total: 0 }) | ||||
|   | ||||
| @@ -16,9 +16,8 @@ export default defineConfig({ | ||||
|       registerType: "autoUpdate", | ||||
|       useCredentials: true, | ||||
|       manifest: { | ||||
|         name: "Solar Network", | ||||
|         short_name: "Solian", | ||||
|         description: "The Solar Network entrypoint.", | ||||
|         name: "Solian", | ||||
|         description: "The Solar Network Application", | ||||
|         theme_color: "#4b5094", | ||||
|         display: "standalone", | ||||
|         icons: [ | ||||
| @@ -27,9 +26,13 @@ export default defineConfig({ | ||||
|             sizes: "1024x1024", | ||||
|             type: "image/png", | ||||
|             purpose: "maskable" | ||||
|           }, | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       workbox: { | ||||
|         sourcemap: true, | ||||
|         cleanupOutdatedCaches: true | ||||
|       } | ||||
|     }) | ||||
|   ], | ||||
|   resolve: { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user