Skip to content

Commit

Permalink
unify hovered links highlighting #100
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed Jan 29, 2019
1 parent d8afa5b commit de249f6
Show file tree
Hide file tree
Showing 17 changed files with 122 additions and 83 deletions.
12 changes: 6 additions & 6 deletions src/electron-main/index.spec.ts
Expand Up @@ -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`);

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions src/electron-main/index.ts
Expand Up @@ -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({
Expand Down Expand Up @@ -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),
Expand Down
@@ -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) {
Expand Down Expand Up @@ -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));
}
Expand All @@ -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;
}
1 change: 1 addition & 0 deletions src/shared/api/main.ts
Expand Up @@ -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;
Expand Down
6 changes: 0 additions & 6 deletions src/web/src/app/_db-view/db-view-mail-body.component.html
Expand Up @@ -113,12 +113,6 @@ <h5>{{ selectedMail.conversationMail.subject }}</h5>

<div class="body-container d-flex flex-grow-1"></div>

<div
*ngIf="hoveredHref$ | async; let hoveredHref;"
[innerText]="hoveredHref"
class="d-flex hovered-href rounded"
></div>

<div
*ngIf="selectedMail.conversationMail.attachments.length"
class="attachments pt-2 mt-2"
Expand Down
15 changes: 1 addition & 14 deletions src/web/src/app/_db-view/db-view-mail-body.component.scss
Expand Up @@ -2,7 +2,6 @@

:host {
$border-color: $list-group-border-color;
$font-size-small: $app-font-size-base-small * 1.2;

position: relative;
display: flex;
Expand All @@ -14,7 +13,7 @@
.attachments {
.badge {
font-weight: normal;
font-size: $font-size-small;
font-size: $app-font-size-base-medium;
color: $gray-700;
border: 1px solid $border-color;
}
Expand Down Expand Up @@ -53,18 +52,6 @@
border-top: 1px solid $border-color;
}

.hovered-href {
position: fixed;
bottom: 0;
left: 0;
margin: $app-spacer-1;
background-color: $gray-700;
color: $white;
border: 1px solid $gray-800;
padding: 0.25em;
font-size: $font-size-small;
}

.controls {
padding: 0 $btn-padding-x ($btn-padding-y * 2) 0;
justify-content: center;
Expand Down
45 changes: 7 additions & 38 deletions src/web/src/app/_db-view/db-view-mail-body.component.ts
Expand Up @@ -9,7 +9,7 @@ import {
QueryList,
ViewChildren,
} from "@angular/core";
import {BehaviorSubject, EMPTY, Subject, Subscription, combineLatest, fromEvent, merge} from "rxjs";
import {BehaviorSubject, EMPTY, Subject, Subscription, combineLatest} from "rxjs";
import {Store} from "@ngrx/store";
import {delay, distinctUntilChanged, filter, map, mergeMap} from "rxjs/operators";

Expand All @@ -30,46 +30,15 @@ export class DbViewMailBodyComponent extends DbViewAbstractComponent implements
selectedMail$ = this.instance$.pipe(
map((value) => 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<Event>();

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<boolean> = new BehaviorSubject(true);

@ViewChildren(DbViewMailComponent, {read: ElementRef})
Expand All @@ -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,
}));
Expand Down
1 change: 0 additions & 1 deletion src/web/src/app/_db-view/db-view-mail.component.html
Expand Up @@ -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 }}
<span *ngIf="mailAddressTotal && mailAddressTotal > 1" class="d-flex ml-1">
Expand Down
2 changes: 1 addition & 1 deletion 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";
Expand Down
2 changes: 2 additions & 0 deletions src/web/src/app/app.module.constants.ts
Expand Up @@ -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";
Expand Down Expand Up @@ -47,6 +48,7 @@ export const APP_MODULE_NG_CONF: NgModule = {
AppComponent,
ErrorItemComponent,
ErrorListComponent,
HoveredHrefComponent,
RouterProxyComponent,
],
providers: [
Expand Down
22 changes: 19 additions & 3 deletions src/web/src/app/components/app.component.spec.ts
Expand Up @@ -4,17 +4,23 @@ 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";

const moduleDef: TestModuleMetadata = Object.freeze({
imports: [
RouterTestingModule,
StoreModule.forRoot({}),
StoreModule.forFeature(OptionsReducer.featureName, OptionsReducer.reducer),
],
declarations: [
AppComponent,
HoveredHrefComponent,
],
declarations: [AppComponent],
});

describe(AppComponent.name, () => {
Expand Down Expand Up @@ -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: () => {},
},
},
];
})));

Expand Down
1 change: 1 addition & 0 deletions src/web/src/app/components/app.component.ts
Expand Up @@ -14,6 +14,7 @@ export type CloseableOutletsType = typeof ERRORS_OUTLET | typeof SETTINGS_OUTLET
<router-outlet name="${ACCOUNTS_OUTLET}"></router-outlet>
<router-outlet name="${SETTINGS_OUTLET}"></router-outlet>
<router-outlet name="${ERRORS_OUTLET}"></router-outlet>
<email-securely-app-hovered-href></email-securely-app-hovered-href>
`,
styleUrls: ["./app.component.scss"],
})
Expand Down
4 changes: 4 additions & 0 deletions src/web/src/app/components/hovered-href.component.html
@@ -0,0 +1,4 @@
<div
*ngIf="targetUrl$ | async; let targetUrl;"
[innerText]="targetUrl"
></div>
21 changes: 21 additions & 0 deletions 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;
}
}
25 changes: 25 additions & 0 deletions 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<State>,
) {}
}

0 comments on commit de249f6

Please sign in to comment.