import { Component, OnInit, AfterViewInit, ViewChild, ElementRef, Inject, LOCALE_ID, EventEmitter, Output } from '@angular/core'; import { ContentManager, ScriptRunnerNew as ScriptRunnerImpl } from 'hatool'; import { HttpClient } from '@angular/common/http'; import { map, catchError, first, switchMap } from 'rxjs/operators'; import { VERSION, PRODUCTION } from '../constants'; import { script } from '../script'; import { of, Subscription } from 'rxjs'; import { ReportStoreService } from '../report-store.service'; import { NotificationService } from '../notification.service'; import { SourceService } from '../source.service'; import { citySuggestions } from '../city-suggestions'; import { MapService } from '../map.service'; import { LayoutService } from '../layout.service'; import { BannerComponent } from '../banner/banner.component'; import { ShareService } from '../share.service'; import { ReminderWidgetComponent } from '../reminder-widget/reminder-widget.component'; import { RemindersService } from '../reminders.service'; import { AppinstallService } from '../appinstall.service'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-chat-page', templateUrl: './chat-page.component.html', styleUrls: ['./chat-page.component.less'] }) export class ChatPageComponent implements OnInit, AfterViewInit { started = false; subscription: Subscription = null; content: ContentManager; runner: ScriptRunnerImpl; toasterMessage = null; bannerMessage = null; bannerButtonMessage = null; reminderWidgetOptions = null; @ViewChild(BannerComponent) banner: BannerComponent; @ViewChild(ReminderWidgetComponent) reminderWidget: ReminderWidgetComponent; @ViewChild('uploadFileText') uploadFileText: ElementRef; @ViewChild('uploadedFileText') uploadedFileText: ElementRef; @ViewChild('notUploadedFileText') notUploadedFileText: ElementRef; @ViewChild('inputPlaceholder') inputPlaceholder: ElementRef; @ViewChild('fixmeMessage') fixmeMessage: ElementRef; @Output() done = new EventEmitter<string>(); constructor(private http: HttpClient, private storage: ReportStoreService, private notifications: NotificationService, private source: SourceService, private mapService: MapService, public layout: LayoutService, private shareService: ShareService, private reminders: RemindersService, private appinstall: AppinstallService, @Inject(LOCALE_ID) private locale) {} init() { this.content = new ContentManager(); this.runner = new ScriptRunnerImpl(this.http, this.content, this.locale); this.runner.timeout = environment.chatRunnerTimeout; this.runner.debug = false; this.runner.fixme = () => { this.restart(); }; } prepareToSave(record) { // filter records fields, to save those that do not start with '_' const result = {}; for (const [key, value] of Object.entries(record)) { if (key[0] !== '_') { result[key] = value; } } return result; } augmentPayload(payload) { payload['version'] = VERSION; payload['_cityTownSuggestions'] = null; return payload; } ngAfterViewInit() { this.content.sendButtonText = ''; this.content.uploadFileText = this.uploadFileText.nativeElement.innerHTML; this.content.uploadedFileText = this.uploadedFileText.nativeElement.innerHTML; this.content.notUploadedFileText = this.notUploadedFileText.nativeElement.innerHTML; this.content.inputPlaceholder = this.inputPlaceholder.nativeElement.innerHTML; this.content.fixmeMessage = this.fixmeMessage.nativeElement.innerHTML; setTimeout(() => { this.start(); }, 0); } ngOnInit() { this.init(); } restart() { const state = this.runner.state; this.subscription.unsubscribe(); this.started = false; this.init(); this.runner.state = state; this.ngAfterViewInit(); } get(obj: any, field) { const parts = field.split('.'); for (const part of parts) { obj = obj[part] || {}; } if (Object.entries(obj).length > 0) { return obj; } return null; } fillIn(record: any, message: string) { return message.replace( RegExp('({{([a-zA-Z_.0-9]+)}})', 'g'), (match, p1, p2) => { return this.get(record, p2) || ''; } ); } selectFields(record, fields) { const ret = {}; for (const re of fields) { const regexp = new RegExp('^' + re + '$'); const keys = Object.keys(record); for (const key of keys) { if (regexp.test(key)) { ret[key] = record[key]; } } } return ret; } makeUid() { let result = ''; const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const charNum = charset.length; for (let i = 0; i < 6; i++) { result += charset.charAt(Math.floor(Math.random() * charNum)); } return result; } start() { if (this.started) { return; } this.started = true; this.subscription = this.runner.run( script, 0, { clear: () => { this.runner.record = Object.assign({}, this.storage.device); }, clear_state: () => { this.runner.state = {}; }, load_local_storage: (record: any) => { record._existing_user = this.storage.reports.length > 0 ? 'returning' : 'new'; }, fetch_previous_reports: (same_address_text, new_address_text, done_text) => { const aliases = {}; const sliceIdx = PRODUCTION ? 10 : 16; const today_prefix = (new Date()).toISOString().slice(0, sliceIdx); const today_aliases = {}; let reported_today = false; for (const report of this.storage.reports) { if (!this.storage.device.originalEngagementSource && report[1].engagementSource) { this.storage.device.originalEngagementSource = report[1].engagementSource; } if (!report[1].alias) { continue; } const alias = report[1].alias; aliases[alias] = report; if (report[0].indexOf(today_prefix) === 0) { today_aliases[alias] = true; } } const options = []; const aliases_list = Object.keys(aliases); this.storage.device.num_aliases = aliases_list.length; for (const alias of aliases_list) { const option: any = {}; if (today_aliases[alias]) { option.class = 'disabled'; reported_today = true; } Object.assign(option, { show: alias, value: this.selectFields(aliases[alias][1], [ 'alias', 'age', 'sex', 'city_town', 'street', 'school_name', 'medical_staff_member', 'is_assisted_living', 'precondition.*', 'hospitalization.*', 'covid_.*', 'insulation.*', 'symptoms_.*_duration', 'general_feeling', 'routine.*', '_household.*', '_public_service_last_reported_yes', 'uid' ]) }); options.push(option); } const lastReport = this.storage.reports.length - 1; options.push({ show: this.fillIn(this.storage.reports[lastReport][1], same_address_text), value: Object.assign(this.selectFields(this.storage.reports[lastReport][1], [ 'city_town', 'street' ]), {_existing_user: 'new'}) }); options.push({ show: new_address_text, value: {_existing_user: 'new'} }); if (reported_today) { options.push({ show: done_text, value: 'done', steps: [ {pop: 'default'} ] }); } return options; }, ensure_uid: (record) => { if (!record['uid']) { record['uid'] = this.makeUid(); } if (!this.storage.device.main_uid) { this.storage.device.main_uid = record['uid']; this.storage.device.main_age = record['age']; this.storage.device.main_city = record['city']; } }, set_flag: (record: any, varname) => { record[varname] = true; }, flag_from_var: (record: any, varname) => { varname = varname || '_var'; if (record[varname]) { record[record[varname]] = true; record[varname] = null; } }, update_from_selection: (record: any, varname) => { Object.assign(record, record[varname]); delete record[varname]; }, fetch_household_data: (record: any) => { try { let _household_adults = null; let _household_minors = null; let _household_data_available = null; for (const report of this.storage.reports) { const r = report[1]; if ((r.city_town === record.city_town) && (r.street === record.street) ) { try { _household_adults = parseInt(r._household_adults, 10); _household_minors = parseInt(r._household_minors, 10); _household_data_available = !!r._household_data_available; } catch (e) { console.log('Bad old report data', r); } } } const household_data = {_household_adults, _household_minors, _household_data_available}; console.log(household_data); Object.assign(record, household_data); } catch (err) { console.error(`household past data check failed: ${err}`); } }, handle_no_covid: (record: any) => { record.covid_positive = record.covid_positive === 'true'; if (!record.covid_positive) { if (record.hospitalization_status) { record.hospitalization_status = false; } for (const k of Object.keys(record)) { if (k.indexOf('symptoms_') >= 0 && k.indexOf('_duration') >= 0) { delete record[k]; } } } }, clear_insulation_status: (record: any) => { record.hospitalization_status = record.hospitalization_status === 'true'; if (record.hospitalization_status && record.insulation_status) { record.insulation_status = 'none'; record.insulation_reason = 'none'; } }, clear_insulation_reason: (record: any) => { record.insulation_reason = record.insulation_reason || 'none'; if (record.insulation_status === 'none') { record.insulation_reason = 'none'; } }, should_check_for_duration: (record: any) => { return !!record.covid_positive || record._existing_user === 'new'; }, need_to_ask_about_routine: (record: any) => { const routine_last_asked = record.routine_last_asked || 0; const timeout = PRODUCTION ? 86400 * 7 * 1000 : 7 * 60 * 1000; const now = Date.now().valueOf(); if (now > routine_last_asked + timeout) { record.routine_last_asked = now; return true; } return false; }, save_public_service_data: (record: any) => { if (record.routine_served_public) { record._public_service_last_reported_yes = Date.now().valueOf(); } const last_yes = record._public_service_last_reported_yes || 0; const timeout = PRODUCTION ? 86400 * 14 * 1000 : 14 * 60 * 1000; const now = Date.now().valueOf(); record.served_public_last_fortnight = (now < last_yes + timeout); console.log('SERVED MORE THAN 10 PEOPLE:', record.served_public_last_fortnight); if (!!record._public_service_last_reported_yes) { console.log('LAST YES REPORT TIME', new Date(record._public_service_last_reported_yes).toISOString()); } }, calculate_met_daily: (record: any) => { try { const _household_data_available = true; Object.assign(record, {_household_data_available}); if (record._met_above_18 || record._met_above_18 === 0 || record._met_above_18 === '0') { let met_above_18 = parseInt(record._household_adults || 0, 10) + parseInt(record._met_above_18 || 0, 10); if (!!record._is_adult) { met_above_18 -= 1; } Object.assign(record, {met_above_18}); } if (record._met_under_18 || record._met_under_18 === 0 || record._met_under_18 === '0') { let met_under_18 = parseInt(record._household_minors || 0, 10) + parseInt(record._met_under_18 || 0, 10); if (!record._is_adult) { met_under_18 -= 1; } Object.assign(record, {met_under_18}); } console.log('MET WITH ADULTS:', record.met_above_18); console.log('MET WITH KIDS:', record.met_under_18); } catch (e) { console.log('Failed to calculate met daily', e); } }, is_adult: (record: any) => { try { const age = parseInt(record.age, 10); console.log('IS ADULT', age, '->', age >= 18); return age >= 18; } catch (err) { console.error(`Age check error: ${err}.\n Age value: ${record.age}`); } }, is_old: (record: any) => { try { const age = parseInt(record.age, 10); return age >= 65; } catch (err) { console.error(`Age check error: ${err}.\n Age value: ${record.age}`); } }, calculate_alias: (record, male_alias, female_alias, other_alias) => { if (record.sex === 'male') { return this.fillIn(record, male_alias); } else if (record.sex === 'female') { return this.fillIn(record, female_alias); } else { return this.fillIn(record, other_alias); } }, combine_location: (record, location_text) => { return this.fillIn(record, location_text).trim(); }, prepare_city_town_suggestions: () => { return citySuggestions[this.locale] || citySuggestions['en']; }, prepare_routine_question_work: (record, s_routine_workplace_is_outside, s_routine_workplace_location, s_routine_workplace_weekly_hours, s_medical_staff_member_true, s_medical_staff_member_false, s_routine_served_public_true, s_routine_served_public_false, ) => { let ret = ''; if (record.routine_workplace_is_outside === undefined) { return 'empty'; } else if (record.routine_workplace_is_outside) { ret += ' - ' + s_routine_workplace_is_outside; if (record.routine_workplace_city_town && record.routine_workplace_street) { ret += ' ' + this.fillIn(record, s_routine_workplace_location); } ret += '<br/>'; if (record.routine_workplace_weekly_hours) { ret += ' - ' + this.fillIn(record, s_routine_workplace_weekly_hours) + '<br/> - '; } if (record.medical_staff_member === undefined) { return 'empty'; } else if (record.medical_staff_member) { ret += s_medical_staff_member_true; } else { ret += s_medical_staff_member_false; } ret += '<br/> - '; if (record.routine_served_public === undefined) { return 'empty'; } else if (record.routine_served_public) { ret += s_routine_served_public_true; } else { ret += s_routine_served_public_false; } } return ret || 'empty'; }, prepare_routine_question_behaviour: (record, s_routine_visits_prayer_house_true, s_routine_visits_prayer_house_false, s_routine_visits_prayer_house_no_response, s_routine_uses_public_transportation_train, s_routine_uses_public_transportation_bus, s_routine_uses_public_transportation_taxi, s_routine_uses_public_transportation_other_only, s_routine_uses_public_transportation_other, s_routine_uses_public_transportation_none, s_routine_wears_mask_always, s_routine_wears_mask_mostly_yes, s_routine_wears_mask_mostly_no, s_routine_wears_mask_never, s_routine_wears_mask_no_response, ) => { let ret = ' - '; if (record.routine_visits_prayer_house === undefined) { return 'empty'; } else if (record.routine_visits_prayer_house === 'no_response') { ret += s_routine_visits_prayer_house_no_response; } else if (record.routine_visits_prayer_house) { ret += s_routine_visits_prayer_house_true; } else { ret += s_routine_visits_prayer_house_false; } let pt = ''; if (record.routine_uses_public_transportation_train) { pt += '<br/> - ' + s_routine_uses_public_transportation_train; } if (record.routine_uses_public_transportation_bus) { pt += '<br/> - ' + s_routine_uses_public_transportation_bus; } if (record.routine_uses_public_transportation_train) { pt += '<br/> - ' + s_routine_uses_public_transportation_taxi; } if (record.routine_uses_public_transportation_other) { if (pt.length > 0) { pt += '<br/> - ' + s_routine_uses_public_transportation_other; } else { pt += '<br/> - ' + s_routine_uses_public_transportation_other_only; } } if (pt.length === 0) { pt += '<br/> - ' + s_routine_uses_public_transportation_none; } ret += pt + '<br/> - '; if (record.routine_wears_mask === undefined) { return 'empty'; } else if (record.routine_wears_mask === 'always') { ret += s_routine_wears_mask_always; } else if (record.routine_wears_mask === 'mostly_yes') { ret += s_routine_wears_mask_mostly_yes; } else if (record.routine_wears_mask === 'mostly_no') { ret += s_routine_wears_mask_mostly_no; } else if (record.routine_wears_mask === 'never') { ret += s_routine_wears_mask_never; } else if (record.routine_wears_mask === 'no_response') { ret += s_routine_wears_mask_no_response; } return ret || 'empty'; }, share_action: async () => { if (this.shareService.shareWidgetSupported) { const shared = this.shareService.shareWidget() .then(() => { return 'shared'; }, () => { return this.shareService.clipboardCopy(); }); if (shared === 'error') { return this.shareService.clipboardCopy(); } return shared; } return this.shareService.clipboardCopy(); }, toaster: (message) => { this.toasterMessage = message; }, banner: async (message, buttonMessage?) => { this.bannerMessage = message; this.bannerButtonMessage = buttonMessage; return new Promise((resolve, reject) => { this.banner.result.pipe(first()).subscribe((value) => { resolve(value); }); }); }, reminder_status: (record) => { const has_reminder = record.action_reminder_wanted === 'already_set' || this.reminders.isSourceReminder(); return has_reminder ? 'not-required' : 'required'; }, reminder_choose_method_show_widget: async (record) => { const options = this.reminders.widgetOptions(record); this.reminderWidgetOptions = options; return new Promise((resolve, reject) => { this.reminderWidget.select.pipe(first()).subscribe((selected) => { resolve(selected); }); }); }, install_notification: async () => { await this.notifications.addNotification(); }, install_app: async (record) => { if (record.action_reminder_selected === 'android-app') { await this.appinstall.prompt('play'); } else if (record.action_reminder_selected === 'iphone-app') { await this.appinstall.prompt('itunes'); } }, install_calendar: () => { window.open('assets/corona_reminder.ics', '_blank'); }, install_telegram: () => { window.open(`https://t.me/coronaisrael_reminder_bot?start=${this.locale}`, '_blank'); }, affiliate_alon_chen_prepare: (later_option) => { const aliases = {}; const sliceIdx = PRODUCTION ? 10 : 16; const today_prefix = (new Date()).toISOString().slice(0, sliceIdx); for (const report of this.storage.reports) { if (!report[1].alias) { continue; } const alias = report[1].alias; if (report[0].indexOf(today_prefix) === 0) { aliases[alias] = report; } } const options = []; const aliases_list = Object.keys(aliases); for (const alias of aliases_list) { const option: any = {}; Object.assign(option, { show: alias, value: aliases[alias][1].uid }); options.push(option); } options.push({ show: later_option, value: 'no-uid', steps: [{pop: 'affiliations'}] }); return options; }, affiliate_alon_chen_action: (record, field_name) => { const links = { he: '1FAIpQLSfbO0mKvS5q5DFEjTrtP6nDsPjeCLjKNknX9Ywzwl7sSTl8jA/viewform?usp=pp_url&entry.356067021=', en: '1FAIpQLSd9HQDsN1WaikNr0QVrdnG7dKWuevbxR2L1v8Uh62mTHM6c4A/viewform?usp=pp_url&entry.101300820=', ar: '1FAIpQLSc0cz03mvaMdEnVqFkzBmmjX-EKWOzYYei2znOh_12yDkkOAA/viewform?usp=pp_url&entry.759991142=', es: '1FAIpQLSdTZcTd0dnv2ZFU-SZ0725qieDoE7ugAECFfQE1gICUm1d05A/viewform?usp=pp_url&entry.2037241339=', fr: '1FAIpQLSf6hU2H_JaiKMCWnGKptxLWFMdAZnUFuik_kIUiFPm2uqp8xQ/viewform?usp=pp_url&entry.1895607011=', ru: '1FAIpQLSfNyO_jtY0K8dWCJ4UvceKgAgcs-BZ4khBYdW55wgq8rFLmnw/viewform?usp=pp_url&entry.2022038576=' }; const uid = record[field_name]; const prefix = 'https://docs.google.com/forms/d/e/'; const link = prefix + (links[this.locale] || links.he) + uid; window.open(link, '_blank'); }, save_report: (record) => { let payload = Object.assign({}, record); payload = this.augmentPayload(payload); this.storage.addReport(payload); payload = this.prepareToSave(payload); let obs = null; if (PRODUCTION) { obs = this.http.post('https://europe-west2-custom-cargo-279912.cloudfunctions.net/avid-covider-reports', payload); } else { console.log('WOULD SEND', payload); window['wouldSend'] = payload; // used by protractor (e2e testing) obs = of({success: true}); } obs.pipe( catchError(() => of({success: false})), map((response: any) => response.success) ).subscribe((success) => { console.log('saved, success=' + success); }); this.mapService.reportedToday = true; } }, (key, value, record) => {} ).pipe( map(() => { let payload = Object.assign({}, this.runner.record); payload = this.augmentPayload(payload); payload = this.prepareToSave(payload); this.storage.saveDevice(payload); return payload; }), switchMap((payload) => { if (PRODUCTION) { return this.http.post('https://europe-west2-custom-cargo-279912.cloudfunctions.net/avid-covider-devices', payload); } else { console.log('DEVICE STATE', payload); return of({success: true}); } }), catchError(() => of({success: false})), map((response: any) => response.success) ).subscribe((success) => { console.log('Reported device success', success); this.done.emit(); }); } }