forked from MapComplete/MapComplete
		
	Add email question, some tweaks
This commit is contained in:
		
						commit
						f67508336a
					
				
					 12 changed files with 123 additions and 69 deletions
				
			
		| 
						 | 
				
			
			@ -9,29 +9,20 @@ import Website from "../Questions/Website";
 | 
			
		|||
import CafeRepair from "../Questions/bike/CafeRepair";
 | 
			
		||||
import CafeDiy from "../Questions/bike/CafeDiy";
 | 
			
		||||
import CafePump from "../Questions/bike/CafePump";
 | 
			
		||||
import {EmailQuestion} from "../Questions/EmailQuestion";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class BikeCafes extends LayerDefinition {
 | 
			
		||||
    private readonly repairsBikes = anyValueExcept("service:bicycle:repair", "no")
 | 
			
		||||
    private readonly hasPump = new Tag("service:bicycle:pump", "yes")
 | 
			
		||||
    private readonly diy = new Tag("service:bicycle:diy", "yes")
 | 
			
		||||
    private readonly bikeServices = [
 | 
			
		||||
        this.diy,
 | 
			
		||||
        this.repairsBikes,
 | 
			
		||||
        this.hasPump
 | 
			
		||||
    ]
 | 
			
		||||
    private readonly to = Translations.t.cyclofix.cafe
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.name = this.to.name;
 | 
			
		||||
        this.icon = "./assets/bike/cafe.svg";
 | 
			
		||||
        super()
 | 
			
		||||
        this.name = this.to.name
 | 
			
		||||
        this.icon = "./assets/bike/cafe.svg"
 | 
			
		||||
        this.overpassFilter = new And([
 | 
			
		||||
            new Tag("amenity", /^pub|bar|cafe$/),
 | 
			
		||||
            new Or([
 | 
			
		||||
                new Regex("amenity", "^pub|bar|cafe")
 | 
			
		||||
            ]),
 | 
			
		||||
            new Or([
 | 
			
		||||
                ...this.bikeServices,
 | 
			
		||||
                new Tag(/^service:bicycle:/, "*"),
 | 
			
		||||
                new Tag("pub", "cycling")
 | 
			
		||||
            ])
 | 
			
		||||
        ]) 
 | 
			
		||||
| 
						 | 
				
			
			@ -49,24 +40,24 @@ export default class BikeCafes extends LayerDefinition {
 | 
			
		|||
        
 | 
			
		||||
        this.maxAllowedOverlapPercentage = 10;
 | 
			
		||||
 | 
			
		||||
        this.minzoom = 13;
 | 
			
		||||
        this.style = this.generateStyleFunction();
 | 
			
		||||
        this.minzoom = 13
 | 
			
		||||
        this.style = this.generateStyleFunction()
 | 
			
		||||
        this.title = new FixedText(this.to.title)
 | 
			
		||||
        this.elementsToShow = [
 | 
			
		||||
            new ImageCarouselWithUploadConstructor(),
 | 
			
		||||
            new CafeName(),
 | 
			
		||||
            new PhoneNumberQuestion("{name}"),
 | 
			
		||||
            new Website("{name}"),
 | 
			
		||||
            new PhoneNumberQuestion("{name}"),
 | 
			
		||||
            new EmailQuestion("{name}"),
 | 
			
		||||
            new CafeRepair(),
 | 
			
		||||
            new CafeDiy(),
 | 
			
		||||
            new CafePump()
 | 
			
		||||
        ];
 | 
			
		||||
        this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY;
 | 
			
		||||
 | 
			
		||||
        ]
 | 
			
		||||
        this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private generateStyleFunction() {
 | 
			
		||||
        const self = this;
 | 
			
		||||
        const self = this
 | 
			
		||||
        return function (properties: any) {
 | 
			
		||||
            return {
 | 
			
		||||
                color: "#00bb00",
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +66,7 @@ export default class BikeCafes extends LayerDefinition {
 | 
			
		|||
                    iconSize: [50, 50],
 | 
			
		||||
                    iconAnchor: [25,50]
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -16,19 +16,6 @@ import Website from "../Questions/Website";
 | 
			
		|||
 | 
			
		||||
export default class BikeOtherShops extends LayerDefinition {
 | 
			
		||||
    private readonly sellsBikes = new Tag("service:bicycle:retail", "yes")
 | 
			
		||||
    private readonly repairsBikes = anyValueExcept("service:bicycle:repair", "no")
 | 
			
		||||
    private readonly rentsBikes = new Tag("service:bicycle:rental", "yes")
 | 
			
		||||
    private readonly hasPump = new Tag("service:bicycle:pump", "yes")
 | 
			
		||||
    private readonly hasDiy = new Tag("service:bicycle:diy", "yes")
 | 
			
		||||
    private readonly sellsSecondHand = anyValueExcept("service:bicycle:repair", "no")
 | 
			
		||||
    private readonly hasBikeServices = new Or([
 | 
			
		||||
        this.sellsBikes,
 | 
			
		||||
        this.repairsBikes,
 | 
			
		||||
        // this.rentsBikes,
 | 
			
		||||
        // this.hasPump,
 | 
			
		||||
        // this.hasDiy,
 | 
			
		||||
        // this.sellsSecondHand
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    private readonly to = Translations.t.cyclofix.nonBikeShop
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +25,7 @@ export default class BikeOtherShops extends LayerDefinition {
 | 
			
		|||
        this.icon = "./assets/bike/non_bike_repair_shop.svg"
 | 
			
		||||
        this.overpassFilter = new And([
 | 
			
		||||
            anyValueExcept("shop", "bicycle"),
 | 
			
		||||
            this.hasBikeServices
 | 
			
		||||
            new Tag(/^service:bicycle:/, "*"),
 | 
			
		||||
        ])
 | 
			
		||||
        this.presets = []
 | 
			
		||||
        this.maxAllowedOverlapPercentage = 10
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import ShopSecondHand from "../Questions/bike/ShopSecondHand";
 | 
			
		|||
import { TagRenderingOptions } from "../TagRendering";
 | 
			
		||||
import {PhoneNumberQuestion} from "../Questions/PhoneNumberQuestion";
 | 
			
		||||
import Website from "../Questions/Website";
 | 
			
		||||
import {EmailQuestion} from "../Questions/EmailQuestion";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class BikeShops extends LayerDefinition {
 | 
			
		||||
| 
						 | 
				
			
			@ -56,8 +57,9 @@ export default class BikeShops extends LayerDefinition {
 | 
			
		|||
        this.elementsToShow = [
 | 
			
		||||
            new ImageCarouselWithUploadConstructor(),
 | 
			
		||||
            new ShopName(),
 | 
			
		||||
            new PhoneNumberQuestion("{name}"),
 | 
			
		||||
            new Website("{name}"),
 | 
			
		||||
            new PhoneNumberQuestion("{name}"),
 | 
			
		||||
            new EmailQuestion("{name}"),
 | 
			
		||||
            new ShopRetail(),
 | 
			
		||||
            new ShopRental(),
 | 
			
		||||
            new ShopRepair(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								Customizations/Questions/EmailQuestion.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Customizations/Questions/EmailQuestion.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
import {TagRenderingOptions} from "../TagRendering";
 | 
			
		||||
import {UIElement} from "../../UI/UIElement";
 | 
			
		||||
import Translations from "../../UI/i18n/Translations";
 | 
			
		||||
 | 
			
		||||
export class EmailQuestion extends TagRenderingOptions {
 | 
			
		||||
 | 
			
		||||
    constructor(category: string | UIElement) {
 | 
			
		||||
        super({
 | 
			
		||||
            question: Translations.t.general.questions.emailOf.Subs({category: category}),
 | 
			
		||||
            freeform: {
 | 
			
		||||
                renderTemplate: Translations.t.general.questions.emailIs.Subs({category: category}),
 | 
			
		||||
                template: "$email$",
 | 
			
		||||
                key: "email"
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -55,24 +55,37 @@ export class Regex extends TagsFilter {
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
export class Tag extends TagsFilter {
 | 
			
		||||
    public key: string
 | 
			
		||||
    public value: string
 | 
			
		||||
    public key: string | RegExp
 | 
			
		||||
    public value: string | RegExp
 | 
			
		||||
    public invertValue: boolean
 | 
			
		||||
 | 
			
		||||
    constructor(key: string, value: string, invertValue = false) {
 | 
			
		||||
    constructor(key: string | RegExp, value: string | RegExp, invertValue = false) {
 | 
			
		||||
        if (value === "*" && invertValue) {
 | 
			
		||||
            throw new Error("Invalid combination: invertValue && value == *")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (value instanceof RegExp && invertValue) {
 | 
			
		||||
            throw new Error("Unsupported combination: RegExp value and inverted value (use regex to invert the match)")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        super()
 | 
			
		||||
        this.key = key
 | 
			
		||||
        this.value = value
 | 
			
		||||
        this.invertValue = invertValue
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (value === "*" && invertValue) {
 | 
			
		||||
            throw new Error("Invalid combination: invertValue && value == *")
 | 
			
		||||
    private static regexOrStrMatches(regexOrStr: string | RegExp, testStr: string) {
 | 
			
		||||
        if (typeof regexOrStr === 'string') {
 | 
			
		||||
            return regexOrStr === testStr
 | 
			
		||||
        } else if (regexOrStr instanceof RegExp) {
 | 
			
		||||
            return (regexOrStr as RegExp).test(testStr)
 | 
			
		||||
        }
 | 
			
		||||
        throw new Error("<regexOrStr> must be of type RegExp or string")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    matches(tags: { k: string; v: string }[]): boolean {
 | 
			
		||||
        for (const tag of tags) {
 | 
			
		||||
            if (tag.k === this.key) {
 | 
			
		||||
            if (Tag.regexOrStrMatches(this.key, tag.k)) {
 | 
			
		||||
                if (tag.v === "") {
 | 
			
		||||
                    // This tag has been removed
 | 
			
		||||
                    return this.value === ""
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +95,7 @@ export class Tag extends TagsFilter {
 | 
			
		|||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return this.value === tag.v !== this.invertValue
 | 
			
		||||
                return Tag.regexOrStrMatches(this.value, tag.v) !== this.invertValue
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -94,19 +107,33 @@ export class Tag extends TagsFilter {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    asOverpass(): string[] {
 | 
			
		||||
        if (this.value === "*") {
 | 
			
		||||
            return ['["' + this.key + '"]'];
 | 
			
		||||
        const keyIsRegex = this.key instanceof RegExp
 | 
			
		||||
        const key = keyIsRegex ? (this.key as RegExp).source : this.key
 | 
			
		||||
 | 
			
		||||
        const valIsRegex = this.value instanceof RegExp
 | 
			
		||||
        const val = valIsRegex ? (this.value as RegExp).source : this.value
 | 
			
		||||
 | 
			
		||||
        const regexKeyPrefix = keyIsRegex ? '~' : ''
 | 
			
		||||
        const anyVal = this.value === "*"
 | 
			
		||||
 | 
			
		||||
        if (anyVal && !keyIsRegex) {
 | 
			
		||||
            return [`[${regexKeyPrefix}"${key}"]`];
 | 
			
		||||
        }
 | 
			
		||||
        if (this.value === "") {
 | 
			
		||||
            // NOT having this key
 | 
			
		||||
            return ['[!"' + this.key + '"]'];
 | 
			
		||||
            return ['[!"' + key + '"]'];
 | 
			
		||||
        }
 | 
			
		||||
        const compareOperator = this.invertValue ? '!=' : '='
 | 
			
		||||
        return ['["' + this.key + '"' + compareOperator + '"' + this.value + '"]'];
 | 
			
		||||
 | 
			
		||||
        const compareOperator = (valIsRegex || keyIsRegex) ? '~' : (this.invertValue ? '!=' : '=')
 | 
			
		||||
        return [`[${regexKeyPrefix}"${key}"${compareOperator}"${keyIsRegex && anyVal ? '.' : val}"]`];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    substituteValues(tags: any) {
 | 
			
		||||
        return new Tag(this.key, TagUtils.ApplyTemplate(this.value, tags));
 | 
			
		||||
        if (typeof this.value !== 'string') {
 | 
			
		||||
            throw new Error("substituteValues() only possible with tag value of type string")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,33 @@
 | 
			
		|||
import {UIElement} from "../UIElement";
 | 
			
		||||
import Translations from "../i18n/Translations";
 | 
			
		||||
import Combine from "./Combine";
 | 
			
		||||
import {link} from "fs";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export class SubtleButton extends UIElement{
 | 
			
		||||
    private imageUrl: string;
 | 
			
		||||
    private message: UIElement;
 | 
			
		||||
    private linkTo: string = undefined;
 | 
			
		||||
 | 
			
		||||
    constructor(imageUrl: string, message: string | UIElement) {
 | 
			
		||||
    constructor(imageUrl: string, message: string | UIElement, linkTo : string = undefined) {
 | 
			
		||||
        super(undefined);
 | 
			
		||||
        this.linkTo = linkTo;
 | 
			
		||||
        this.message = Translations.W(message);
 | 
			
		||||
        this.imageUrl = imageUrl;
 | 
			
		||||
        
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    InnerRender(): string {
 | 
			
		||||
 | 
			
		||||
        if(this.linkTo != undefined){
 | 
			
		||||
            return new Combine([
 | 
			
		||||
                `<a class="subtle-button" href="${this.linkTo}" target="_blank">`,
 | 
			
		||||
                this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "",
 | 
			
		||||
                this.message,
 | 
			
		||||
                '</a>'
 | 
			
		||||
            ]).Render();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return new Combine([
 | 
			
		||||
            '<span class="subtle-button">',
 | 
			
		||||
            this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ export class LayerSelection extends UIElement{
 | 
			
		|||
          this._checkboxes.push(new CheckBox(
 | 
			
		||||
              new Combine([checkbox, icon, name]),
 | 
			
		||||
              new Combine([
 | 
			
		||||
                  Img.checkmark,
 | 
			
		||||
                  Img.no_checkmark,
 | 
			
		||||
                  icon,
 | 
			
		||||
                  layer.layerDef.name]),
 | 
			
		||||
              layer.isDisplayed));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,16 +34,12 @@ export class MoreScreen extends UIElement {
 | 
			
		|||
            const link =
 | 
			
		||||
                new SubtleButton(layout.icon,
 | 
			
		||||
                    new Combine([
 | 
			
		||||
                        `<a href="${linkText}" target="_blank">`,
 | 
			
		||||
                        "<div>",
 | 
			
		||||
                        "<b>",
 | 
			
		||||
                        Translations.W(layout.title),
 | 
			
		||||
                        "</b>",
 | 
			
		||||
                        "<br/>",
 | 
			
		||||
                        Translations.W(layout.description),
 | 
			
		||||
                        "</div>",
 | 
			
		||||
                        "</a>"
 | 
			
		||||
                    ]));
 | 
			
		||||
                    ]), linkText);
 | 
			
		||||
 | 
			
		||||
            els.push(link)
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,8 @@ export class SimpleAddUI extends UIElement {
 | 
			
		|||
        = new UIEventSource<Preset>(undefined);
 | 
			
		||||
    private confirmButton: UIElement = undefined;
 | 
			
		||||
    private cancelButton: UIElement;
 | 
			
		||||
    private goToInboxButton: UIElement = new SubtleButton("/assets/envelope.svg", 
 | 
			
		||||
        Translations.t.general.goToInbox, "https://www.openstreetmap.org/messages/inbox");
 | 
			
		||||
 | 
			
		||||
    constructor(zoomlevel: UIEventSource<{ zoom: number }>,
 | 
			
		||||
                lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
 | 
			
		||||
| 
						 | 
				
			
			@ -132,7 +134,9 @@ export class SimpleAddUI extends UIElement {
 | 
			
		|||
        if (this._userDetails.data.unreadMessages > 0) {
 | 
			
		||||
            return new Combine([header, "<span class='alert'>",
 | 
			
		||||
                Translations.t.general.readYourMessages,
 | 
			
		||||
                "</span>"]).Render();
 | 
			
		||||
                "</span>",
 | 
			
		||||
                this.goToInboxButton
 | 
			
		||||
            ]).Render();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this._userDetails.data.dryRun) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -748,7 +748,16 @@ export default class Translations {
 | 
			
		|||
                websiteIs: new T({
 | 
			
		||||
                    en: "Website: <a href='{website}' target='_blank'>{website}</a>",
 | 
			
		||||
                    nl: "Website: <a href='{website}' target='_blank'>{website}</a>"
 | 
			
		||||
                })
 | 
			
		||||
                }),
 | 
			
		||||
                emailOf: new T({
 | 
			
		||||
                        en: "What is the email address of {category}?",
 | 
			
		||||
                        nl: "Wat is het email-adres van {category}?"
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
                emailIs: new T({
 | 
			
		||||
                    en: "The email address of this {category} is <a href='mailto:{email}' target='_blank'>{email}</a>",
 | 
			
		||||
                    nl: "Het email-adres van {category} is <a href='mailto:{email}' target='_blank'>{email}</a>"
 | 
			
		||||
                }),
 | 
			
		||||
 | 
			
		||||
            },
 | 
			
		||||
            openStreetMapIntro: new T({
 | 
			
		||||
| 
						 | 
				
			
			@ -794,6 +803,10 @@ export default class Translations {
 | 
			
		|||
            fewChangesBefore: new T({
 | 
			
		||||
                en: "Please, answer a few questions of existing points before adding a new point.",
 | 
			
		||||
                nl: "Gelieve eerst enkele vragen van bestaande punten te beantwoorden vooraleer zelf punten toe te voegen."
 | 
			
		||||
            }),
 | 
			
		||||
            goToInbox: new T({
 | 
			
		||||
                en: "Open inbox",
 | 
			
		||||
                nl: "Ga naar de berichten"
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								index.css
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								index.css
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -4,10 +4,6 @@ html, body {
 | 
			
		|||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a {
 | 
			
		||||
    text-decoration: unset;
 | 
			
		||||
    color:unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    font-family: 'Helvetica Neue', Arial, sans-serif;
 | 
			
		||||
| 
						 | 
				
			
			@ -1206,6 +1202,13 @@ form {
 | 
			
		|||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.subtle-button a {
 | 
			
		||||
    text-decoration: unset !important;
 | 
			
		||||
    color:unset !important;
 | 
			
		||||
    display: block ruby;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.subtle-button img{
 | 
			
		||||
    width: 3em;
 | 
			
		||||
    max-height: 3em;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								index.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -105,11 +105,11 @@ const fullScreenMessage = new UIEventSource<UIElement>(undefined);
 | 
			
		|||
// The latest element that was selected - used to generate the right UI at the right place
 | 
			
		||||
const selectedElement = new UIEventSource<{ feature: any }>(undefined);
 | 
			
		||||
 | 
			
		||||
const zoom = QueryParameters.GetQueryParameter("z", "" + layoutToUse.startzoom)
 | 
			
		||||
const zoom = QueryParameters.GetQueryParameter("z", undefined)
 | 
			
		||||
    .syncWith(LocalStorageSource.Get("zoom"));
 | 
			
		||||
const lat = QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat)
 | 
			
		||||
const lat = QueryParameters.GetQueryParameter("lat", undefined)
 | 
			
		||||
    .syncWith(LocalStorageSource.Get("lat"));
 | 
			
		||||
const lon = QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon)
 | 
			
		||||
const lon = QueryParameters.GetQueryParameter("lon", undefined)
 | 
			
		||||
    .syncWith(LocalStorageSource.Get("lon"));
 | 
			
		||||
 | 
			
		||||
const featureSwitchUserbadge = QueryParameters.GetQueryParameter("fs-userbadge", ""+layoutToUse.enableUserBadge);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue