From de249f6759c09cebb74ca237ae6daa3d33aa756e Mon Sep 17 00:00:00 2001 From: Vladimir Y Date: Tue, 29 Jan 2019 11:32:50 +0300 Subject: [PATCH] unify hovered links highlighting #100 --- src/electron-main/index.spec.ts | 12 ++--- src/electron-main/index.ts | 4 +- ...ontent-context-menu.ts => web-contents.ts} | 32 +++++++++---- src/shared/api/main.ts | 1 + .../_db-view/db-view-mail-body.component.html | 6 --- .../_db-view/db-view-mail-body.component.scss | 15 +------ .../_db-view/db-view-mail-body.component.ts | 45 +++---------------- .../app/_db-view/db-view-mail.component.html | 1 - .../src/app/_shared/unread-badge.component.ts | 2 +- src/web/src/app/app.module.constants.ts | 2 + .../src/app/components/app.component.spec.ts | 22 +++++++-- src/web/src/app/components/app.component.ts | 1 + .../components/hovered-href.component.html | 4 ++ .../components/hovered-href.component.scss | 21 +++++++++ .../app/components/hovered-href.component.ts | 25 +++++++++++ src/web/src/util.ts | 10 +++-- src/web/src/variables.scss | 2 +- 17 files changed, 122 insertions(+), 83 deletions(-) rename src/electron-main/{web-content-context-menu.ts => web-contents.ts} (66%) create mode 100644 src/web/src/app/components/hovered-href.component.html create mode 100644 src/web/src/app/components/hovered-href.component.scss create mode 100644 src/web/src/app/components/hovered-href.component.ts diff --git a/src/electron-main/index.spec.ts b/src/electron-main/index.spec.ts index f043e5e59..03a1c89c5 100644 --- a/src/electron-main/index.spec.ts +++ b/src/electron-main/index.spec.ts @@ -44,9 +44,9 @@ test.serial("workflow", async (t) => { t.true(m["./api"].initApi.calledWithExactly(t.context.ctx), `"initApi" called`); t.true(m["./api"].initApi.calledAfter(m["./web-request"].initWebRequestListeners), `"initApi" called after "initWebRequestListeners"`); - t.true(m["./web-content-context-menu"].initWebContentContextMenu.calledWithExactly(), `"initWebContentContextMenu" called`); - t.true(m["./web-content-context-menu"].initWebContentContextMenu.calledBefore(m["./window"].initBrowserWindow), `"initWebContentContextMenu" called before "initBrowserWindow"`); - t.true(m["./web-content-context-menu"].initWebContentContextMenu.calledBefore(m["./tray"].initTray), `"initWebContentContextMenu" called before "initTray"`); + t.true(m["./web-contents"].initWebContentsCreatingHandlers.calledWithExactly(), `"initWebContentsCreatingHandlers" called`); + t.true(m["./web-contents"].initWebContentsCreatingHandlers.calledBefore(m["./window"].initBrowserWindow), `"initWebContentsCreatingHandlers" called before "initBrowserWindow"`); + t.true(m["./web-contents"].initWebContentsCreatingHandlers.calledBefore(m["./tray"].initTray), `"initWebContentsCreatingHandlers" called before "initTray"`); t.true(m["./window"].initBrowserWindow.calledWithExactly(t.context.ctx, t.context.endpoints), `"initBrowserWindow" called`); @@ -95,7 +95,7 @@ test.beforeEach(async (t) => { mock(() => import("./window")).callThrough().with(mocks["./window"]); mock(() => import("./tray")).callThrough().with(mocks["./tray"]); mock(() => import("./menu")).callThrough().with(mocks["./menu"]); - mock(() => import("./web-content-context-menu")).with(mocks["./web-content-context-menu"]); + mock(() => import("./web-contents")).with(mocks["./web-contents"]); mock(() => import("./app-update")).callThrough().with(mocks["./app-update"]); mock(() => import("./keytar")).with({ getPassword: sinon.spy(), @@ -133,8 +133,8 @@ function buildMocks(testContext: TestContext) { "./menu": { initApplicationMenu: sinon.spy(), }, - "./web-content-context-menu": { - initWebContentContextMenu: sinon.spy(), + "./web-contents": { + initWebContentsCreatingHandlers: sinon.spy(), }, "./app-update": { initAutoUpdate: sinon.spy(), diff --git a/src/electron-main/index.ts b/src/electron-main/index.ts index 08faafb5a..12f496702 100644 --- a/src/electron-main/index.ts +++ b/src/electron-main/index.ts @@ -11,7 +11,7 @@ import {initBrowserWindow} from "./window"; import {initContext} from "./util"; import {initDefaultSession} from "./session"; import {initTray} from "./tray"; -import {initWebContentContextMenu} from "./web-content-context-menu"; +import {initWebContentsCreatingHandlers} from "./web-contents"; import {initWebRequestListeners} from "./web-request"; electronUnhandled({ @@ -39,7 +39,7 @@ app.on("ready", async () => { const endpoints = await initApi(ctx); const {checkForUpdatesAndNotify} = await endpoints.readConfig().toPromise(); - initWebContentContextMenu(); + initWebContentsCreatingHandlers(); ctx.uiContext = { browserWindow: await initBrowserWindow(ctx, endpoints), diff --git a/src/electron-main/web-content-context-menu.ts b/src/electron-main/web-contents.ts similarity index 66% rename from src/electron-main/web-content-context-menu.ts rename to src/electron-main/web-contents.ts index 8a24c9812..6a7c05822 100644 --- a/src/electron-main/web-content-context-menu.ts +++ b/src/electron-main/web-contents.ts @@ -1,11 +1,16 @@ import {ContextMenuParams, Event, Menu, MenuItemConstructorOptions, WebContents, app, clipboard} from "electron"; import {platform} from "os"; +import {IPC_MAIN_API_NOTIFICATION$} from "./api/constants"; +import {IPC_MAIN_API_NOTIFICATION_ACTIONS} from "src/shared/api/main"; + const emptyArray = Object.freeze([]); -const contextMenuEventSubscriptionArgs: ["context-menu", (event: Event, params: ContextMenuParams) => void] = [ - "context-menu", - ({sender: webContents}: Event, {editFlags, linkURL, linkText}: ContextMenuParams) => { +const eventSubscriptions: { + "context-menu": (event: Event, params: ContextMenuParams) => void; + "update-target-url": (event: Event, url: string) => void; +} = { + "context-menu": ({sender: webContents}: Event, {editFlags, linkURL, linkText}) => { const template: MenuItemConstructorOptions[] = []; if (linkURL) { @@ -34,14 +39,23 @@ const contextMenuEventSubscriptionArgs: ["context-menu", (event: Event, params: Menu.buildFromTemplate(template).popup({}); } }, -]; + "update-target-url": (event, url) => { + IPC_MAIN_API_NOTIFICATION$.next(IPC_MAIN_API_NOTIFICATION_ACTIONS.TargetUrl({url})); + }, +}; const webContentsCreatedHandler = (webContents: WebContents) => { - webContents.removeListener(...contextMenuEventSubscriptionArgs); - webContents.on(...contextMenuEventSubscriptionArgs); + Object + .entries(eventSubscriptions) + .map(([event, handler]) => { + // TODO TS: get rid of any casting + webContents.removeListener(event as any, handler); + webContents.on(event as any, handler); + }); }; -export function initWebContentContextMenu() { +// WARN: needs to be called before "BrowserWindow" creating +export function initWebContentsCreatingHandlers() { app.on("browser-window-created", (event, {webContents}) => webContentsCreatedHandler(webContents)); app.on("web-contents-created", (webContentsCreatedEvent, webContents) => webContentsCreatedHandler(webContents)); } @@ -51,5 +65,7 @@ function isEmailHref(href: string): boolean { } function extractEmailIfEmailHref(href: string): string { - return isEmailHref(href) ? String(href.split("mailto:").pop()) : href; + return isEmailHref(href) + ? String(href.split("mailto:").pop()) + : href; } diff --git a/src/shared/api/main.ts b/src/shared/api/main.ts index 57301dfaf..2bd0b404f 100644 --- a/src/shared/api/main.ts +++ b/src/shared/api/main.ts @@ -165,6 +165,7 @@ export const IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS = unionize({ export const IPC_MAIN_API_NOTIFICATION_ACTIONS = unionize({ Bootstrap: ofType<{}>(), ActivateBrowserWindow: ofType<{}>(), + TargetUrl: ofType<{ url: string }>(), DbPatchAccount: ofType<{ key: DbModel.DbAccountPk; entitiesModified: boolean; diff --git a/src/web/src/app/_db-view/db-view-mail-body.component.html b/src/web/src/app/_db-view/db-view-mail-body.component.html index f72cdf5ab..6f8dffd0d 100644 --- a/src/web/src/app/_db-view/db-view-mail-body.component.html +++ b/src/web/src/app/_db-view/db-view-mail-body.component.html @@ -113,12 +113,6 @@
{{ selectedMail.conversationMail.subject }}
-
-
value.selectedMail), mergeMap((value) => value ? [value] : EMPTY), - distinctUntilChanged((prev, curr) => { - return ( - prev.rootNode.entryPk === curr.rootNode.entryPk - && - prev.conversationMail.pk === curr.conversationMail.pk - ); - }), + distinctUntilChanged((prev, curr) => ( + prev.rootNode.entryPk === curr.rootNode.entryPk + && + prev.conversationMail.pk === curr.conversationMail.pk + )), ); iframeBodyEventSubject$ = new Subject(); - hoveredHref$ = merge( - merge( - fromEvent(this.elementRef.nativeElement as HTMLElement, "mouseover"), - this.iframeBodyEventSubject$.pipe( - filter(({type}) => type === "mouseover"), - ), - ).pipe( - map((event) => { - const {link, href} = this.resolveLinkHref(event.target as Element); - - if (!link || !href) { - return false; - } - - return href; - }), - ), - merge( - fromEvent(this.elementRef.nativeElement as HTMLElement, "mouseout"), - this.iframeBodyEventSubject$.pipe( - filter(({type}) => type === "mouseout"), - ), - ).pipe( - map(() => false), - ), - ).pipe( - distinctUntilChanged(), - ); - conversationCollapsed$: BehaviorSubject = new BehaviorSubject(true); @ViewChildren(DbViewMailComponent, {read: ElementRef}) @@ -83,7 +52,7 @@ export class DbViewMailBodyComponent extends DbViewAbstractComponent implements this.zone.run(() => this.iframeBodyEventSubject$.next(event)); }); - private readonly bodyIframeEventArgs = ["click", "mouseover", "mouseout"].map((event) => ({ + private readonly bodyIframeEventArgs = ["click"].map((event) => ({ event, handler: this.bodyIframeEventHandler, })); diff --git a/src/web/src/app/_db-view/db-view-mail.component.html b/src/web/src/app/_db-view/db-view-mail.component.html index 7284a1fbb..4833b047f 100644 --- a/src/web/src/app/_db-view/db-view-mail.component.html +++ b/src/web/src/app/_db-view/db-view-mail.component.html @@ -13,7 +13,6 @@ *ngIf="mailAddress" class="prevent-default-event address badge flex-row mr-2" href="mailto:{{ mailAddress.address }}" - title="{{ mailAddress.name ? mailAddress.name + ' <' + mailAddress.address + '>': '' }}" > {{ mailAddress.name || mailAddress.address }} diff --git a/src/web/src/app/_shared/unread-badge.component.ts b/src/web/src/app/_shared/unread-badge.component.ts index 96abc53bd..6a2f9aea6 100644 --- a/src/web/src/app/_shared/unread-badge.component.ts +++ b/src/web/src/app/_shared/unread-badge.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2} from "@angular/core"; import {Store, select} from "@ngrx/store"; -import {Subscription} from "rxjs/Subscription"; +import {Subscription} from "rxjs"; import {OptionsSelectors} from "src/web/src/app/store/selectors"; import {State} from "src/web/src/app/store/reducers/options"; diff --git a/src/web/src/app/app.module.constants.ts b/src/web/src/app/app.module.constants.ts index 0ffb9fcfb..33d1189a4 100644 --- a/src/web/src/app/app.module.constants.ts +++ b/src/web/src/app/app.module.constants.ts @@ -20,6 +20,7 @@ import {AppErrorHandler} from "src/web/src/app/app.error-handler.service"; import {CoreModule} from "./_core/core.module"; import {ErrorItemComponent} from "./components/error-item.component"; import {ErrorListComponent} from "./components/error-list.component"; +import {HoveredHrefComponent} from "./components/hovered-href.component"; import {RouterProxyComponent} from "./components/router-proxy.component"; import {RoutingModule} from "./app.routing.module"; import {getMetaReducers, reducers} from "./store/reducers/root"; @@ -47,6 +48,7 @@ export const APP_MODULE_NG_CONF: NgModule = { AppComponent, ErrorItemComponent, ErrorListComponent, + HoveredHrefComponent, RouterProxyComponent, ], providers: [ diff --git a/src/web/src/app/components/app.component.spec.ts b/src/web/src/app/components/app.component.spec.ts index af6cd5457..af27ca5df 100644 --- a/src/web/src/app/components/app.component.spec.ts +++ b/src/web/src/app/components/app.component.spec.ts @@ -4,8 +4,10 @@ import {RouterTestingModule} from "@angular/router/testing"; import {Store, StoreModule} from "@ngrx/store"; import {produce} from "immer"; +import * as OptionsReducer from "src/web/src/app/store/reducers/options"; import {AppComponent} from "./app.component"; import {ESC_KEY, SETTINGS_OUTLET as outlet} from "src/web/src/app/app.constants"; +import {HoveredHrefComponent} from "./hovered-href.component"; import {NAVIGATION_ACTIONS} from "src/web/src/app/store/actions"; import {initTestEnvironment} from "src/web/test/util"; @@ -13,8 +15,12 @@ const moduleDef: TestModuleMetadata = Object.freeze({ imports: [ RouterTestingModule, StoreModule.forRoot({}), + StoreModule.forFeature(OptionsReducer.featureName, OptionsReducer.reducer), + ], + declarations: [ + AppComponent, + HoveredHrefComponent, ], - declarations: [AppComponent], }); describe(AppComponent.name, () => { @@ -65,8 +71,18 @@ describe(AppComponent.name, () => { testBed.resetTestingModule(); testBed = initTestEnvironment((tb) => tb.configureTestingModule(produce(moduleDef, (draftState) => { draftState.providers = [ - {provide: Location, useValue: {path: locationPathStub}}, - {provide: Store, useValue: {dispatch: storeDispatchStub}}, + { + provide: Location, useValue: { + path: locationPathStub, + }, + }, + { + provide: Store, useValue: { + dispatch: storeDispatchStub, + // TODO mock "pipe" with sinon stub + pipe: () => {}, + }, + }, ]; }))); diff --git a/src/web/src/app/components/app.component.ts b/src/web/src/app/components/app.component.ts index 3772be2ff..e86145196 100644 --- a/src/web/src/app/components/app.component.ts +++ b/src/web/src/app/components/app.component.ts @@ -14,6 +14,7 @@ export type CloseableOutletsType = typeof ERRORS_OUTLET | typeof SETTINGS_OUTLET + `, styleUrls: ["./app.component.scss"], }) diff --git a/src/web/src/app/components/hovered-href.component.html b/src/web/src/app/components/hovered-href.component.html new file mode 100644 index 000000000..66762a372 --- /dev/null +++ b/src/web/src/app/components/hovered-href.component.html @@ -0,0 +1,4 @@ +
diff --git a/src/web/src/app/components/hovered-href.component.scss b/src/web/src/app/components/hovered-href.component.scss new file mode 100644 index 000000000..984487df1 --- /dev/null +++ b/src/web/src/app/components/hovered-href.component.scss @@ -0,0 +1,21 @@ +@import "~src/web/src/variables"; + +:host { + $bg-color: theme-color("secondary"); + $padding: 0.2em; + $padding-l: 0.25em; + + > div { + @include border-radius; + display: flex; + position: fixed; + left: 0; + bottom: 0; + background-color: $bg-color; + border: 1px solid darken($bg-color, 10%); + color: $white; + font-size: $app-font-size-base-medium; + margin: $padding-l $padding; + padding: $padding $padding-l; + } +} diff --git a/src/web/src/app/components/hovered-href.component.ts b/src/web/src/app/components/hovered-href.component.ts new file mode 100644 index 000000000..14ed51686 --- /dev/null +++ b/src/web/src/app/components/hovered-href.component.ts @@ -0,0 +1,25 @@ +import {ChangeDetectionStrategy, Component} from "@angular/core"; +import {Store, select} from "@ngrx/store"; +import {filter, map} from "rxjs/operators"; + +import {FEATURED} from "src/web/src/app/store/selectors/options"; +import {IPC_MAIN_API_NOTIFICATION_ACTIONS} from "src/shared/api/main"; +import {State} from "src/web/src/app/store/reducers/root"; + +@Component({ + selector: "email-securely-app-hovered-href", + templateUrl: "./hovered-href.component.html", + styleUrls: ["./hovered-href.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HoveredHrefComponent { + targetUrl$ = this.store.pipe( + select(FEATURED.mainProcessNotification), + filter(IPC_MAIN_API_NOTIFICATION_ACTIONS.is.TargetUrl), + map(({payload: {url}}) => url), + ); + + constructor( + private store: Store, + ) {} +} diff --git a/src/web/src/util.ts b/src/web/src/util.ts index ac0c91a40..4cd423b43 100644 --- a/src/web/src/util.ts +++ b/src/web/src/util.ts @@ -6,15 +6,16 @@ import {curryFunctionMembers} from "src/shared/util"; // TODO ban direct "__ELECTRON_EXPOSURE__.buildLoggerBundle" referencing in tslint, but only via "getZoneNameBoundWebLogger" call -export type ZoneNameBoundWebLogger = typeof LOGGER & { zoneName: () => string }; +type ZoneNameBoundWebLogger = typeof LOGGER & { zoneName: () => string }; -export const LOGGER = __ELECTRON_EXPOSURE__.buildLoggerBundle("[WEB]"); +const LOGGER = __ELECTRON_EXPOSURE__.buildLoggerBundle("[WEB]"); -export const formatZoneName = () => `<${Zone.current.name}>`; +const formatZoneName = () => `<${Zone.current.name}>`; export const getZoneNameBoundWebLogger = (...args: string[]): ZoneNameBoundWebLogger => { const logger = curryFunctionMembers(LOGGER, ...args); const zoneName = formatZoneName; + for (const level of LOG_LEVELS) { logger[level] = ((original) => { return function(this: typeof logger) { @@ -22,6 +23,7 @@ export const getZoneNameBoundWebLogger = (...args: string[]): ZoneNameBoundWebLo }; })(logger[level]); } + return {...logger, zoneName}; }; @@ -31,7 +33,9 @@ export const logActionTypeAndBoundLoggerWithActionType =

{ type: string; payload: P } & { logger: ZoneNameBoundWebLogger } => { return (aciton) => { const logger = curryFunctionMembers(_logger, JSON.stringify({actionType: aciton.type})); + logger[level](); + return { ...aciton, logger, diff --git a/src/web/src/variables.scss b/src/web/src/variables.scss index d5067e87d..1e87e6b14 100644 --- a/src/web/src/variables.scss +++ b/src/web/src/variables.scss @@ -11,7 +11,7 @@ $app-color-bg-dark: #5a6786; $app-color-warning-light-text: theme-color-level("warning", 6); $app-font-size-base-small: $font-size-base * 0.75; -$app-font-size-base-medium: $font-size-base * 0.85; +$app-font-size-base-medium: $font-size-base * 0.9; @mixin app-dropdown-toggle-split { padding-left: $btn-padding-x * 1.1;