Merge pull request #11735 from vvlladd28/improvement/dashboard/ios-support-context-menu
Add support long tap in iOS device (show widget/dashboard contexе menu)
This commit is contained in:
		
						commit
						21fb4afb24
					
				@ -59,7 +59,7 @@
 | 
			
		||||
    "flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master",
 | 
			
		||||
    "font-awesome": "^4.7.0",
 | 
			
		||||
    "html2canvas": "^1.4.1",
 | 
			
		||||
    "jquery": "^3.6.3",
 | 
			
		||||
    "jquery": "^3.7.1",
 | 
			
		||||
    "jquery.terminal": "^2.35.3",
 | 
			
		||||
    "js-beautify": "1.14.7",
 | 
			
		||||
    "json-schema-defaults": "^0.4.0",
 | 
			
		||||
@ -127,7 +127,7 @@
 | 
			
		||||
    "@types/flowjs": "^2.13.9",
 | 
			
		||||
    "@types/jasmine": "~3.10.2",
 | 
			
		||||
    "@types/jasminewd2": "^2.0.10",
 | 
			
		||||
    "@types/jquery": "^3.5.16",
 | 
			
		||||
    "@types/jquery": "^3.5.30",
 | 
			
		||||
    "@types/js-beautify": "^1.13.3",
 | 
			
		||||
    "@types/leaflet": "1.8.0",
 | 
			
		||||
    "@types/leaflet-polylinedecorator": "1.6.4",
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ import { AuthService } from '@core/auth/auth.service';
 | 
			
		||||
import { svgIcons, svgIconsUrl } from '@shared/models/icon.models';
 | 
			
		||||
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
 | 
			
		||||
import { SETTINGS_KEY } from '@core/settings/settings.effects';
 | 
			
		||||
import { initCustomJQueryEvents } from '@shared/models/jquery-event.models';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-root',
 | 
			
		||||
@ -74,6 +75,8 @@ export class AppComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    this.setupTranslate();
 | 
			
		||||
    this.setupAuth();
 | 
			
		||||
 | 
			
		||||
    initCustomJQueryEvents();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setupTranslate() {
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@
 | 
			
		||||
                [class.center-vertical]="centerVertical"
 | 
			
		||||
                [class.center-horizontal]="centerHorizontal"
 | 
			
		||||
                (mousedown)="onDashboardMouseDown($event)"
 | 
			
		||||
                (contextmenu)="openDashboardContextMenu($event)">
 | 
			
		||||
                (tbcontextmenu)="openDashboardContextMenu($event)">
 | 
			
		||||
  <div #dashboardMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
 | 
			
		||||
       [style.left]="dashboardMenuPosition.x"
 | 
			
		||||
       [style.top]="dashboardMenuPosition.y"
 | 
			
		||||
 | 
			
		||||
@ -58,6 +58,7 @@ import { WidgetComponentAction, WidgetComponentActionType } from '@home/componen
 | 
			
		||||
import { TbPopoverComponent } from '@shared/components/popover.component';
 | 
			
		||||
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
 | 
			
		||||
import { coerceBoolean } from '@shared/decorators/coercion';
 | 
			
		||||
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-dashboard',
 | 
			
		||||
@ -187,13 +188,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
 | 
			
		||||
 | 
			
		||||
  dashboardMenuPosition = { x: '0px', y: '0px' };
 | 
			
		||||
 | 
			
		||||
  dashboardContextMenuEvent: MouseEvent;
 | 
			
		||||
  dashboardContextMenuEvent: TbContextMenuEvent;
 | 
			
		||||
 | 
			
		||||
  @ViewChild('widgetMenuTrigger', {static: true}) widgetMenuTrigger: MatMenuTrigger;
 | 
			
		||||
 | 
			
		||||
  widgetMenuPosition = { x: '0px', y: '0px' };
 | 
			
		||||
 | 
			
		||||
  widgetContextMenuEvent: MouseEvent;
 | 
			
		||||
  widgetContextMenuEvent: TbContextMenuEvent;
 | 
			
		||||
 | 
			
		||||
  dashboardWidgets = new DashboardWidgets(this,
 | 
			
		||||
    this.differs.find([]).create<Widget>((_, item) => item),
 | 
			
		||||
@ -395,7 +396,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openDashboardContextMenu($event: MouseEvent) {
 | 
			
		||||
  openDashboardContextMenu($event: TbContextMenuEvent) {
 | 
			
		||||
    if (this.callbacks && this.callbacks.prepareDashboardContextMenu) {
 | 
			
		||||
      const items = this.callbacks.prepareDashboardContextMenu($event);
 | 
			
		||||
      if (items && items.length) {
 | 
			
		||||
@ -410,7 +411,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) {
 | 
			
		||||
  private openWidgetContextMenu($event: TbContextMenuEvent, widget: DashboardWidget) {
 | 
			
		||||
    if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
 | 
			
		||||
      const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.isReference);
 | 
			
		||||
      if (items && items.length) {
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,7 @@ import { UtilsService } from '@core/services/utils.service';
 | 
			
		||||
import { from } from 'rxjs';
 | 
			
		||||
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
 | 
			
		||||
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
 | 
			
		||||
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
 | 
			
		||||
 | 
			
		||||
export enum WidgetComponentActionType {
 | 
			
		||||
  MOUSE_DOWN,
 | 
			
		||||
@ -57,7 +58,7 @@ export enum WidgetComponentActionType {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class WidgetComponentAction {
 | 
			
		||||
  event: MouseEvent;
 | 
			
		||||
  event: MouseEvent | TbContextMenuEvent;
 | 
			
		||||
  actionType: WidgetComponentActionType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -151,7 +152,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
 | 
			
		||||
    }
 | 
			
		||||
    $(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent));
 | 
			
		||||
    $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent));
 | 
			
		||||
    $(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent));
 | 
			
		||||
    $(this.gridsterItem.el).on('tbcontextmenu', (e: TbContextMenuEvent) => this.onContextMenu(e));
 | 
			
		||||
    const dashboardContentElement = this.widget.widgetContext.dashboardContentElement;
 | 
			
		||||
    if (dashboardContentElement) {
 | 
			
		||||
      this.initEditWidgetActionTooltip(dashboardContentElement);
 | 
			
		||||
@ -180,6 +181,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
 | 
			
		||||
    if (this.editWidgetActionsTooltip && !this.editWidgetActionsTooltip.status().destroyed) {
 | 
			
		||||
      this.editWidgetActionsTooltip.destroy();
 | 
			
		||||
    }
 | 
			
		||||
    $(this.gridsterItem.el).off('mousedown');
 | 
			
		||||
    $(this.gridsterItem.el).off('click');
 | 
			
		||||
    $(this.gridsterItem.el).off('tbcontextmenu');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isHighlighted(widget: DashboardWidget) {
 | 
			
		||||
@ -219,7 +223,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onContextMenu(event: MouseEvent) {
 | 
			
		||||
  onContextMenu(event: TbContextMenuEvent) {
 | 
			
		||||
    if (event) {
 | 
			
		||||
      event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -185,7 +185,7 @@
 | 
			
		||||
                  matTooltipPosition="above">
 | 
			
		||||
            <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
 | 
			
		||||
          </button>
 | 
			
		||||
          <div class="tb-absolute-fill tb-rulechain-graph" (contextmenu)="openRuleChainContextMenu($event)">
 | 
			
		||||
          <div class="tb-absolute-fill tb-rulechain-graph" (tbcontextmenu)="openRuleChainContextMenu($event)">
 | 
			
		||||
            <div #ruleChainMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
 | 
			
		||||
                 [style.left]="ruleChainMenuPosition.x"
 | 
			
		||||
                 [style.top]="ruleChainMenuPosition.y"
 | 
			
		||||
 | 
			
		||||
@ -94,6 +94,7 @@ import { VersionControlComponent } from '@home/components/vc/version-control.com
 | 
			
		||||
import { ComponentClusteringMode } from '@shared/models/component-descriptor.models';
 | 
			
		||||
import { MatDrawer } from '@angular/material/sidenav';
 | 
			
		||||
import { HttpStatusCode } from '@angular/common/http';
 | 
			
		||||
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
 | 
			
		||||
import Timeout = NodeJS.Timeout;
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@ -131,7 +132,7 @@ export class RuleChainPageComponent extends PageComponent
 | 
			
		||||
 | 
			
		||||
  ruleChainMenuPosition = { x: '0px', y: '0px' };
 | 
			
		||||
 | 
			
		||||
  contextMenuEvent: MouseEvent;
 | 
			
		||||
  contextMenuEvent: TbContextMenuEvent;
 | 
			
		||||
 | 
			
		||||
  ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors;
 | 
			
		||||
  ruleNodeTypesLibraryArray = ruleNodeTypesLibrary;
 | 
			
		||||
@ -657,7 +658,7 @@ export class RuleChainPageComponent extends PageComponent
 | 
			
		||||
    this.validate();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openRuleChainContextMenu($event: MouseEvent) {
 | 
			
		||||
  openRuleChainContextMenu($event: TbContextMenuEvent) {
 | 
			
		||||
    if (this.ruleChainCanvas.modelService && !$event.ctrlKey && !$event.metaKey) {
 | 
			
		||||
      const x = $event.clientX;
 | 
			
		||||
      const y = $event.clientY;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								ui-ngx/src/app/shared/directives/context-menu.directive.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								ui-ngx/src/app/shared/directives/context-menu.directive.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
///
 | 
			
		||||
/// Copyright © 2016-2024 The Thingsboard Authors
 | 
			
		||||
///
 | 
			
		||||
/// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
/// you may not use this file except in compliance with the License.
 | 
			
		||||
/// You may obtain a copy of the License at
 | 
			
		||||
///
 | 
			
		||||
///     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
///
 | 
			
		||||
/// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
/// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
/// See the License for the specific language governing permissions and
 | 
			
		||||
/// limitations under the License.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import { Directive, ElementRef, EventEmitter, OnDestroy, Output } from '@angular/core';
 | 
			
		||||
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
 | 
			
		||||
 | 
			
		||||
@Directive({
 | 
			
		||||
  selector: '[tbcontextmenu]'
 | 
			
		||||
})
 | 
			
		||||
export class ContextMenuDirective implements OnDestroy {
 | 
			
		||||
 | 
			
		||||
  @Output()
 | 
			
		||||
  tbcontextmenu = new EventEmitter<TbContextMenuEvent>();
 | 
			
		||||
 | 
			
		||||
  constructor(private el: ElementRef) {
 | 
			
		||||
    $(this.el.nativeElement).on('tbcontextmenu', (e: TbContextMenuEvent) => this.tbcontextmenu.emit(e));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    $(this.el.nativeElement).off('tbcontextmenu');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -16,3 +16,4 @@
 | 
			
		||||
 | 
			
		||||
export * from './truncate-with-tooltip.directive';
 | 
			
		||||
export * from './ellipsis-chip-list.directive';
 | 
			
		||||
export * from './context-menu.directive';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										80
									
								
								ui-ngx/src/app/shared/models/jquery-event.models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								ui-ngx/src/app/shared/models/jquery-event.models.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
			
		||||
///
 | 
			
		||||
/// Copyright © 2016-2024 The Thingsboard Authors
 | 
			
		||||
///
 | 
			
		||||
/// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
/// you may not use this file except in compliance with the License.
 | 
			
		||||
/// You may obtain a copy of the License at
 | 
			
		||||
///
 | 
			
		||||
///     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
///
 | 
			
		||||
/// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
/// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
/// See the License for the specific language governing permissions and
 | 
			
		||||
/// limitations under the License.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import Timeout = NodeJS.Timeout;
 | 
			
		||||
 | 
			
		||||
export interface TbContextMenuEvent extends Event {
 | 
			
		||||
  clientX: number;
 | 
			
		||||
  clientY: number;
 | 
			
		||||
  ctrlKey: boolean;
 | 
			
		||||
  metaKey: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const isIOSDevice = (): boolean =>
 | 
			
		||||
  /iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.userAgent.includes('Mac') && 'ontouchend' in document);
 | 
			
		||||
 | 
			
		||||
export const initCustomJQueryEvents = () => {
 | 
			
		||||
  $.event.special.tbcontextmenu = {
 | 
			
		||||
    setup(this: HTMLElement) {
 | 
			
		||||
      const el = $(this);
 | 
			
		||||
      if (isIOSDevice()) {
 | 
			
		||||
        let timeoutId: Timeout;
 | 
			
		||||
 | 
			
		||||
        el.on('touchstart', (e) => {
 | 
			
		||||
          e.stopPropagation();
 | 
			
		||||
          timeoutId = setTimeout(() => {
 | 
			
		||||
            timeoutId = null;
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            const touch = e.originalEvent.changedTouches[0];
 | 
			
		||||
            const event = $.Event('tbcontextmenu', {
 | 
			
		||||
              clientX: touch.clientX,
 | 
			
		||||
              clientY: touch.clientY,
 | 
			
		||||
              ctrlKey: false,
 | 
			
		||||
              metaKey: false
 | 
			
		||||
            });
 | 
			
		||||
            el.trigger(event, e);
 | 
			
		||||
          }, 500);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        el.on('touchend touchmove', () => {
 | 
			
		||||
          if (timeoutId) {
 | 
			
		||||
            clearTimeout(timeoutId);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        el.on('contextmenu', (e) => {
 | 
			
		||||
          e.preventDefault();
 | 
			
		||||
          e.stopPropagation();
 | 
			
		||||
          const event = $.Event('tbcontextmenu', {
 | 
			
		||||
            clientX: e.originalEvent.clientX,
 | 
			
		||||
            clientY: e.originalEvent.clientY,
 | 
			
		||||
            ctrlKey: e.originalEvent.ctrlKey,
 | 
			
		||||
            metaKey: e.originalEvent.metaKey,
 | 
			
		||||
          });
 | 
			
		||||
          el.trigger(event, e);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    teardown(this: HTMLElement) {
 | 
			
		||||
      const el = $(this);
 | 
			
		||||
      if (isIOSDevice()) {
 | 
			
		||||
        el.off('touchstart touchend touchmove');
 | 
			
		||||
      } else {
 | 
			
		||||
        el.off('contextmenu');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -68,6 +68,7 @@ import { NgxHmCarouselModule } from 'ngx-hm-carousel';
 | 
			
		||||
import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
 | 
			
		||||
import { UserMenuComponent } from '@shared/components/user-menu.component';
 | 
			
		||||
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
 | 
			
		||||
import { ContextMenuDirective } from '@shared/directives/context-menu.directive';
 | 
			
		||||
import { NospacePipe } from '@shared/pipe/nospace.pipe';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component';
 | 
			
		||||
@ -371,6 +372,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
 | 
			
		||||
    LedLightComponent,
 | 
			
		||||
    MarkdownEditorComponent,
 | 
			
		||||
    TruncateWithTooltipDirective,
 | 
			
		||||
    ContextMenuDirective,
 | 
			
		||||
    NospacePipe,
 | 
			
		||||
    MillisecondsToTimeStringPipe,
 | 
			
		||||
    EnumToArrayPipe,
 | 
			
		||||
@ -633,6 +635,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
 | 
			
		||||
    LedLightComponent,
 | 
			
		||||
    MarkdownEditorComponent,
 | 
			
		||||
    TruncateWithTooltipDirective,
 | 
			
		||||
    ContextMenuDirective,
 | 
			
		||||
    NospacePipe,
 | 
			
		||||
    MillisecondsToTimeStringPipe,
 | 
			
		||||
    EnumToArrayPipe,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								ui-ngx/src/typings/jquery.typings.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								ui-ngx/src/typings/jquery.typings.d.ts
									
									
									
									
										vendored
									
									
								
							@ -14,6 +14,9 @@
 | 
			
		||||
/// limitations under the License.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
 | 
			
		||||
 | 
			
		||||
interface JQuery {
 | 
			
		||||
  terminal(options?: any): any;
 | 
			
		||||
  on(events: 'tbcontextmenu', handler: (e: TbContextMenuEvent) => void): this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3052,10 +3052,10 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/jasmine" "*"
 | 
			
		||||
 | 
			
		||||
"@types/jquery@*", "@types/jquery@^3.5.16", "@types/jquery@^3.5.29":
 | 
			
		||||
  version "3.5.29"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.29.tgz#3c06a1f519cd5fc3a7a108971436c00685b5dcea"
 | 
			
		||||
  integrity sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==
 | 
			
		||||
"@types/jquery@*", "@types/jquery@^3.5.29", "@types/jquery@^3.5.30":
 | 
			
		||||
  version "3.5.30"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.30.tgz#888d584cbf844d3df56834b69925085038fd80f7"
 | 
			
		||||
  integrity sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/sizzle" "*"
 | 
			
		||||
 | 
			
		||||
@ -7377,7 +7377,7 @@ jquery.terminal@^2.35.3:
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    fsevents "^2.3.2"
 | 
			
		||||
 | 
			
		||||
jquery@>=1.9.1, jquery@^3.5.0, jquery@^3.6.3, jquery@^3.7.1:
 | 
			
		||||
jquery@>=1.9.1, jquery@^3.5.0, jquery@^3.7.1:
 | 
			
		||||
  version "3.7.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"
 | 
			
		||||
  integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user