commit
						4f54afca3c
					
				| @ -12,7 +12,7 @@ module.exports = { | ||||
|     transform: { | ||||
|         '^.+\\.(ts|html)$': 'ts-jest', | ||||
|     }, | ||||
|     transformIgnorePatterns: ['node_modules/(?!@ionic-native|@ionic)'], | ||||
|     transformIgnorePatterns: ['node_modules/(?!@ionic-native|@ionic|@moodlehq/ionic-native-push)'], | ||||
|     moduleNameMapper: { | ||||
|         ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src/' }), | ||||
|         '^!raw-loader!.*': 'jest-raw-loader', | ||||
|  | ||||
							
								
								
									
										44
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										44
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -4197,21 +4197,6 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/push": { | ||||
|       "version": "5.36.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/push/-/push-5.36.0.tgz", | ||||
|       "integrity": "sha512-N2Ei6qsIYOmqfz/kH9XpKeIp3C5Qe9NXebzH2ytkpwBApPiCc6h+9LOxgMB/rls9VfT0V0ZoxvJbac9UZ6SJmA==", | ||||
|       "requires": { | ||||
|         "@types/cordova": "^0.0.34" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/cordova": { | ||||
|           "version": "0.0.34", | ||||
|           "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", | ||||
|           "integrity": "sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/qr-scanner": { | ||||
|       "version": "5.36.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/qr-scanner/-/qr-scanner-5.36.0.tgz", | ||||
| @ -11588,10 +11573,33 @@ | ||||
|       "resolved": "https://registry.npmjs.org/@moodlehq/cordova-plugin-zip/-/cordova-plugin-zip-3.1.0-moodle.1.tgz", | ||||
|       "integrity": "sha512-QD5S6bsm6awJrNMb8YN/vkYghKAMfZMHccdimx6s1i5S9fgZUSf7L477NJqjFu62imVZYJIJuavBbw5fR/562w==" | ||||
|     }, | ||||
|     "@moodlehq/ionic-native-push": { | ||||
|       "version": "5.36.0-moodle.2", | ||||
|       "resolved": "https://registry.npmjs.org/@moodlehq/ionic-native-push/-/ionic-native-push-5.36.0-moodle.2.tgz", | ||||
|       "integrity": "sha512-UWT4WaoTEEqGQ5pu+CyakXCOhiXsQSb8mD8j89jDqV0hJyrIQ8zA2ciGW9Y/vd55NuqjCu1tNJcWEJ4WcRVv0Q==", | ||||
|       "requires": { | ||||
|         "@angular/core": "^9.1.12", | ||||
|         "@ionic-native/core": "^5.1.0", | ||||
|         "@types/cordova": "^11.0.0", | ||||
|         "rxjs": "^5.5.0 || ^6.5.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@angular/core": { | ||||
|           "version": "9.1.13", | ||||
|           "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.1.13.tgz", | ||||
|           "integrity": "sha512-mBm24Q9GjkAsxMAzqQ86U1078+yTEpr0+syMEruUtJ0HUH6Fzn3J+6xTLb+BVcGb9RkCkFaV9T5mcn6ZM0f++g==" | ||||
|         }, | ||||
|         "@types/cordova": { | ||||
|           "version": "11.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-11.0.0.tgz", | ||||
|           "integrity": "sha512-AtBm1IAqqXsXszJe6XxuA2iXLhraNCj25p/FHRyikPeW0Z3YfgM6qzWb+VJglJTmZc5lqRNy84cYM/sQI5v6Vw==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@moodlehq/phonegap-plugin-push": { | ||||
|       "version": "4.0.0-moodle.2", | ||||
|       "resolved": "https://registry.npmjs.org/@moodlehq/phonegap-plugin-push/-/phonegap-plugin-push-4.0.0-moodle.2.tgz", | ||||
|       "integrity": "sha512-kxHnpCzM7VMw5XUrLeZX03bLkQzA3j//+4nq7MiZbLoliPsQRAxGqyZ9HmbLcPsvlt1h7NM1eSVG52qZ7D3PlQ==" | ||||
|       "version": "4.0.0-moodle.3", | ||||
|       "resolved": "https://registry.npmjs.org/@moodlehq/phonegap-plugin-push/-/phonegap-plugin-push-4.0.0-moodle.3.tgz", | ||||
|       "integrity": "sha512-oJTmcVN6QBxo8+9uHEFTLCgNJkd7jeaT1MMM3ljDhR5EkFDkHwMMat/Km0tjm+9ToD0LYWHZfvLljpCZM5u3yg==" | ||||
|     }, | ||||
|     "@mrmlnc/readdir-enhanced": { | ||||
|       "version": "2.2.1", | ||||
|  | ||||
| @ -65,7 +65,6 @@ | ||||
|     "@ionic-native/local-notifications": "5.36.0", | ||||
|     "@ionic-native/media-capture": "5.36.0", | ||||
|     "@ionic-native/network": "5.36.0", | ||||
|     "@ionic-native/push": "5.36.0", | ||||
|     "@ionic-native/qr-scanner": "5.36.0", | ||||
|     "@ionic-native/splash-screen": "5.36.0", | ||||
|     "@ionic-native/sqlite": "5.36.0", | ||||
| @ -80,7 +79,8 @@ | ||||
|     "@moodlehq/cordova-plugin-local-notification": "0.9.0-moodle.11", | ||||
|     "@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.5", | ||||
|     "@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1", | ||||
|     "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.2", | ||||
|     "@moodlehq/ionic-native-push": "5.36.0-moodle.2", | ||||
|     "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.3", | ||||
|     "@ngx-translate/core": "13.0.0", | ||||
|     "@ngx-translate/http-loader": "6.0.0", | ||||
|     "@types/chart.js": "2.9.31", | ||||
|  | ||||
| @ -30,7 +30,7 @@ import { WebView } from '@ionic-native/ionic-webview/ngx'; | ||||
| import { Keyboard } from '@ionic-native/keyboard/ngx'; | ||||
| import { LocalNotifications } from '@ionic-native/local-notifications/ngx'; | ||||
| import { MediaCapture } from '@ionic-native/media-capture/ngx'; | ||||
| import { Push } from '@ionic-native/push/ngx'; | ||||
| import { Push } from '@moodlehq/ionic-native-push/ngx'; | ||||
| import { QRScanner } from '@ionic-native/qr-scanner/ngx'; | ||||
| import { SplashScreen } from '@ionic-native/splash-screen/ngx'; | ||||
| import { SQLite } from '@ionic-native/sqlite/ngx'; | ||||
|  | ||||
| @ -12,6 +12,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { SQLiteDB } from '@classes/sqlitedb'; | ||||
| import { CoreAppSchema } from '@services/app'; | ||||
| import { CoreSiteSchema } from '@services/sites'; | ||||
| 
 | ||||
| @ -21,7 +22,7 @@ import { CoreSiteSchema } from '@services/sites'; | ||||
|  */ | ||||
| export const BADGE_TABLE_NAME = 'addon_pushnotifications_badge'; | ||||
| export const PENDING_UNREGISTER_TABLE_NAME = 'addon_pushnotifications_pending_unregister'; | ||||
| export const REGISTERED_DEVICES_TABLE_NAME = 'addon_pushnotifications_registered_devices'; | ||||
| export const REGISTERED_DEVICES_TABLE_NAME = 'addon_pushnotifications_registered_devices_2'; | ||||
| export const APP_SCHEMA: CoreAppSchema = { | ||||
|     name: 'CorePushNotificationsProvider', | ||||
|     version: 1, | ||||
| @ -70,7 +71,7 @@ export const APP_SCHEMA: CoreAppSchema = { | ||||
| }; | ||||
| export const SITE_SCHEMA: CoreSiteSchema = { | ||||
|     name: 'AddonPushNotificationsProvider', | ||||
|     version: 1, | ||||
|     version: 2, | ||||
|     tables: [ | ||||
|         { | ||||
|             name: REGISTERED_DEVICES_TABLE_NAME, | ||||
| @ -103,10 +104,20 @@ export const SITE_SCHEMA: CoreSiteSchema = { | ||||
|                     name: 'pushid', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'publickey', | ||||
|                     type: 'TEXT', | ||||
|                 }, | ||||
|             ], | ||||
|             primaryKeys: ['appid', 'uuid'], | ||||
|         }, | ||||
|     ], | ||||
|     async migrate(db: SQLiteDB, oldVersion: number): Promise<void> { | ||||
|         if (oldVersion < 2) { | ||||
|             // Schema changed in v4.2.
 | ||||
|             await db.migrateTable('addon_pushnotifications_registered_devices', REGISTERED_DEVICES_TABLE_NAME); | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
| @ -139,4 +150,5 @@ export type CorePushNotificationsRegisteredDeviceDBRecord = { | ||||
|     platform: string; // Device platform.
 | ||||
|     version: string; // Device version.
 | ||||
|     pushid: string; // Push ID.
 | ||||
|     publickey?: string; // Public key.
 | ||||
| }; | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { ILocalNotification } from '@ionic-native/local-notifications'; | ||||
| import { NotificationEventResponse, PushOptions, RegistrationEventResponse } from '@ionic-native/push/ngx'; | ||||
| import { NotificationEventResponse, PushOptions, RegistrationEventResponse } from '@moodlehq/ionic-native-push/ngx'; | ||||
| 
 | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| @ -103,10 +103,14 @@ export class CorePushNotificationsProvider { | ||||
| 
 | ||||
|         // Register device on Moodle site when login.
 | ||||
|         CoreEvents.on(CoreEvents.LOGIN, async () => { | ||||
|             if (!this.canRegisterOnMoodle()) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 await this.registerDeviceOnMoodle(); | ||||
|             } catch (error) { | ||||
|                 this.logger.warn('Can\'t register device', error); | ||||
|                 this.logger.error('Can\'t register device', error); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
| @ -305,11 +309,11 @@ export class CorePushNotificationsProvider { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get data to register the device in Moodle. | ||||
|      * Get required data to register the device in Moodle. | ||||
|      * | ||||
|      * @returns Data. | ||||
|      */ | ||||
|     protected getRegisterData(): CoreUserAddUserDeviceWSParams { | ||||
|     protected getRequiredRegisterData(): CoreUserAddUserDeviceWSParams { | ||||
|         if (!this.pushID) { | ||||
|             throw new CoreError('Cannot get register data because pushID is not set.'); | ||||
|         } | ||||
| @ -581,7 +585,7 @@ export class CorePushNotificationsProvider { | ||||
| 
 | ||||
|         await CoreUtils.ignoreErrors(Promise.all([ | ||||
|             // Remove the device from the local DB.
 | ||||
|             this.registeredDevicesTables[site.getId()].delete(this.getRegisterData()), | ||||
|             this.registeredDevicesTables[site.getId()].delete(this.getRequiredRegisterData()), | ||||
|             // Remove pending unregisters for this site.
 | ||||
|             this.pendingUnregistersTable.deleteByPrimaryKey({ siteid: site.getId() }), | ||||
|         ])); | ||||
| @ -680,12 +684,12 @@ export class CorePushNotificationsProvider { | ||||
|                 // Execute the callback in the Angular zone, so change detection doesn't stop working.
 | ||||
|                 NgZone.run(() => { | ||||
|                     this.pushID = data.registrationId; | ||||
|                     if (!CoreSites.isLoggedIn()) { | ||||
|                     if (!CoreSites.isLoggedIn() || !this.canRegisterOnMoodle()) { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     this.registerDeviceOnMoodle().catch((error) => { | ||||
|                         this.logger.warn('Can\'t register device', error); | ||||
|                         this.logger.error('Can\'t register device', error); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
| @ -721,30 +725,48 @@ export class CorePushNotificationsProvider { | ||||
| 
 | ||||
|         try { | ||||
| 
 | ||||
|             const data = this.getRegisterData(); | ||||
|             let result = { | ||||
|                 unregister: true, | ||||
|                 register: true, | ||||
|             }; | ||||
|             const data = this.getRequiredRegisterData(); | ||||
|             data.publickey = await this.getPublicKey(site); | ||||
| 
 | ||||
|             if (!forceUnregister) { | ||||
|                 // Check if the device is already registered.
 | ||||
|                 result = await this.shouldRegister(data, site); | ||||
|             } | ||||
|             const neededActions = await this.getRegisterDeviceActions(data, site, forceUnregister); | ||||
| 
 | ||||
|             if (result.unregister) { | ||||
|             if (neededActions.unregister) { | ||||
|                 // Unregister the device first.
 | ||||
|                 await CoreUtils.ignoreErrors(this.unregisterDeviceOnMoodle(site)); | ||||
|             } | ||||
| 
 | ||||
|             if (result.register) { | ||||
|             if (neededActions.register) { | ||||
|                 // Now register the device.
 | ||||
|                 await site.write('core_user_add_user_device', CoreUtils.clone(data)); | ||||
|                 const addDeviceResponse = | ||||
|                     await site.write<CoreUserAddUserDeviceWSResponse>('core_user_add_user_device', CoreUtils.clone(data)); | ||||
| 
 | ||||
|                 const deviceAlreadyRegistered = | ||||
|                     addDeviceResponse[0] && addDeviceResponse[0].find(warning => warning.warningcode === 'existingkeyforthisuser'); | ||||
|                 if (deviceAlreadyRegistered && data.publickey) { | ||||
|                     // Device already registered, make sure the public key is up to date.
 | ||||
|                     await this.updatePublicKeyOnMoodle(site, data); | ||||
|                 } | ||||
| 
 | ||||
|                 CoreEvents.trigger(CoreEvents.DEVICE_REGISTERED_IN_MOODLE, {}, site.getId()); | ||||
| 
 | ||||
|                 // Insert the device in the local DB.
 | ||||
|                 await CoreUtils.ignoreErrors(this.registeredDevicesTables[site.getId()].insert(data)); | ||||
|             } else if (neededActions.updatePublicKey) { | ||||
|                 // Device already registered, make sure the public key is up to date.
 | ||||
|                 const response = await this.updatePublicKeyOnMoodle(site, data); | ||||
| 
 | ||||
|                 if (response?.warnings?.find(warning => warning.warningcode === 'devicedoesnotexist')) { | ||||
|                     // The device doesn't exist in the server. Remove the device from the local DB and try again.
 | ||||
|                     await this.registeredDevicesTables[site.getId()].delete({ | ||||
|                         appid: data.appid, | ||||
|                         uuid: data.uuid, | ||||
|                         name: data.name, | ||||
|                         model: data.model, | ||||
|                         platform: data.platform, | ||||
|                     }); | ||||
| 
 | ||||
|                     await this.registerDeviceOnMoodle(siteId, false); | ||||
|                 } | ||||
|             } | ||||
|         } finally { | ||||
|             // Remove pending unregisters for this site.
 | ||||
| @ -752,6 +774,48 @@ export class CorePushNotificationsProvider { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the public key to register in a site. | ||||
|      * | ||||
|      * @param site Site to register | ||||
|      * @returns Public key, undefined if the site or the device doesn't support encryption. | ||||
|      */ | ||||
|     protected async getPublicKey(site: CoreSite): Promise<string | undefined> { | ||||
|         if (!site.isVersionGreaterEqualThan('4.2')) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const publicKey = await Push.getPublicKey(); | ||||
| 
 | ||||
|         return publicKey ?? undefined; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update a public key on a Moodle site. | ||||
|      * | ||||
|      * @param site Site. | ||||
|      * @param data Device data. | ||||
|      * @returns WS response, undefined if no public key. | ||||
|      */ | ||||
|     protected async updatePublicKeyOnMoodle( | ||||
|         site: CoreSite, | ||||
|         data: CoreUserAddUserDeviceWSParams, | ||||
|     ): Promise<CoreUserUpdateUserDevicePublicKeyWSResponse | undefined> { | ||||
|         if (!data.publickey) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Update public key on Moodle.'); | ||||
| 
 | ||||
|         const params: CoreUserUpdateUserDevicePublicKeyWSParams = { | ||||
|             uuid: data.uuid, | ||||
|             appid: data.appid, | ||||
|             publickey: data.publickey, | ||||
|         }; | ||||
| 
 | ||||
|         return await site.write<CoreUserUpdateUserDevicePublicKeyWSResponse>('core_user_update_user_device_public_key', params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the addon/site badge counter from the database. | ||||
|      * | ||||
| @ -812,16 +876,26 @@ export class CorePushNotificationsProvider { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if device should be registered (and unregistered first). | ||||
|      * Get the needed actions to perform to register a device. | ||||
|      * | ||||
|      * @param data Data of the device. | ||||
|      * @param site Site to use. | ||||
|      * @returns Promise resolved with booleans: whether to register/unregister. | ||||
|      * @param forceUnregister Whether to force unregister and register. | ||||
|      * @returns Whether each action needs to be performed or not. | ||||
|      */ | ||||
|     protected async shouldRegister( | ||||
|     protected async getRegisterDeviceActions( | ||||
|         data: CoreUserAddUserDeviceWSParams, | ||||
|         site: CoreSite, | ||||
|     ): Promise<{register: boolean; unregister: boolean}> { | ||||
|         forceUnregister?: boolean, | ||||
|     ): Promise<RegisterDeviceActions> { | ||||
|         if (forceUnregister) { | ||||
|             // No need to check if device is stored, always unregister and register the device.
 | ||||
|             return { | ||||
|                 unregister: true, | ||||
|                 register: true, | ||||
|                 updatePublicKey: false, | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         // Check if the device is already registered.
 | ||||
|         const records = await CoreUtils.ignoreErrors( | ||||
| @ -836,35 +910,24 @@ export class CorePushNotificationsProvider { | ||||
| 
 | ||||
|         let isStored = false; | ||||
|         let versionOrPushChanged = false; | ||||
|         let updatePublicKey = false; | ||||
| 
 | ||||
|         (records || []).forEach((record) => { | ||||
|             if (record.version == data.version && record.pushid == data.pushid) { | ||||
|                 // The device is already stored.
 | ||||
|                 isStored = true; | ||||
|                 updatePublicKey = !!data.publickey && record.publickey !== data.publickey; | ||||
|             } else { | ||||
|                 // The version or pushid has changed.
 | ||||
|                 versionOrPushChanged = true; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         if (isStored) { | ||||
|             // The device has already been registered, no need to register it again.
 | ||||
|             return { | ||||
|                 register: false, | ||||
|                 unregister: false, | ||||
|             }; | ||||
|         } else if (versionOrPushChanged) { | ||||
|             // This data can be updated by calling register WS, no need to call unregister.
 | ||||
|             return { | ||||
|                 register: true, | ||||
|                 unregister: false, | ||||
|             }; | ||||
|         } else { | ||||
|             return { | ||||
|                 register: true, | ||||
|                 unregister: true, | ||||
|             }; | ||||
|         } | ||||
|         return { | ||||
|             register: !isStored, // No need to register if device is already stored.
 | ||||
|             unregister: !isStored && !versionOrPushChanged, // No need to unregister first if only version or push changed.
 | ||||
|             updatePublicKey, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -930,9 +993,33 @@ export type CoreUserAddUserDeviceWSParams = { | ||||
|     version: string; // The device version '6.1.2' or '4.2.2' etc.
 | ||||
|     pushid: string; // The device PUSH token/key/identifier/registration id.
 | ||||
|     uuid: string; // The device UUID.
 | ||||
|     publickey?: string; // @since 4.2. The app generated public key.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by core_user_add_user_device WS. | ||||
|  */ | ||||
| export type CoreUserAddUserDeviceWSResponse = CoreWSExternalWarning[][]; | ||||
| 
 | ||||
| /** | ||||
|  * Params of core_user_update_user_device_public_key WS. | ||||
|  */ | ||||
| export type CoreUserUpdateUserDevicePublicKeyWSParams = { | ||||
|     uuid: string; | ||||
|     appid: string; | ||||
|     publickey: string; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by core_user_update_user_device_public_key WS. | ||||
|  */ | ||||
| export type CoreUserUpdateUserDevicePublicKeyWSResponse = { | ||||
|     status: boolean; | ||||
|     warnings?: CoreWSExternalWarning[]; | ||||
| }; | ||||
| 
 | ||||
| type RegisterDeviceActions = { | ||||
|     register: boolean; // Whether device needs to be registered in LMS.
 | ||||
|     unregister: boolean; // Whether device needs to be unregistered before register in LMS to make sure data is up to date.
 | ||||
|     updatePublicKey: boolean; // Whether only public key needs to be updated.
 | ||||
| }; | ||||
|  | ||||
| @ -53,7 +53,7 @@ import { WebView as WebViewService } from '@ionic-native/ionic-webview/ngx'; | ||||
| import { Keyboard as KeyboardService } from '@ionic-native/keyboard/ngx'; | ||||
| import { LocalNotifications as LocalNotificationsService } from '@ionic-native/local-notifications/ngx'; | ||||
| import { MediaCapture as MediaCaptureService } from '@ionic-native/media-capture/ngx'; | ||||
| import { Push as PushService } from '@ionic-native/push/ngx'; | ||||
| import { Push as PushService } from '@moodlehq/ionic-native-push/ngx'; | ||||
| import { QRScanner as QRScannerService } from '@ionic-native/qr-scanner/ngx'; | ||||
| import { StatusBar as StatusBarService } from '@ionic-native/status-bar/ngx'; | ||||
| import { SplashScreen as SplashScreenService } from '@ionic-native/splash-screen/ngx'; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user