Skip to content

Commit

Permalink
reflect selected account title to main app window title #215
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed Nov 25, 2019
1 parent 2c9bf2a commit d862618
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 123 deletions.
5 changes: 4 additions & 1 deletion src/electron-main/storage-upgrade.ts
Expand Up @@ -191,12 +191,15 @@ const CONFIG_UPGRADES: Record<string, (config: Config) => void> = {
config.localDbMailsListViewMode = INITIAL_STORES.config().localDbMailsListViewMode;
}
},
"3.8.1": ({timeouts}) => {
"3.8.1": ({timeouts, ...restConfig}) => {
// force default "indexingBootstrap" timeout to be the minimum value
timeouts.indexingBootstrap = Math.max(
INITIAL_STORES.config().timeouts.indexingBootstrap,
timeouts.indexingBootstrap,
);
if (typeof restConfig.reflectSelectedAccountTitle === "undefined") {
restConfig.reflectSelectedAccountTitle = INITIAL_STORES.config().reflectSelectedAccountTitle;
}
},
};

Expand Down
261 changes: 143 additions & 118 deletions src/electron-preload/webview/protonmail/api/index.ts
Expand Up @@ -226,136 +226,161 @@ const endpoints: ProtonmailApi = {

logger.info();

type TitleOutput = Required<Pick<ProtonmailNotificationOutput, "title">>;
type LoggedInOutput = Required<Pick<ProtonmailNotificationOutput, "loggedIn">>;
type PageTypeOutput = Required<Pick<ProtonmailNotificationOutput, "pageType">>;
type UnreadOutput = Required<Pick<ProtonmailNotificationOutput, "unread">>;
type BatchEntityUpdatesCounterOutput = Required<Pick<ProtonmailNotificationOutput, "batchEntityUpdatesCounter">>;

const observables: [
Observable<LoggedInOutput>,
Observable<PageTypeOutput>,
Observable<UnreadOutput>,
Observable<BatchEntityUpdatesCounterOutput>
] = [
interval(WebviewConstants.NOTIFICATION_LOGGED_IN_POLLING_INTERVAL).pipe(
map(() => isLoggedIn()),
distinctUntilChanged(),
map((loggedIn) => ({loggedIn})),
),

// TODO listen for location.href change instead of starting polling interval
interval(WebviewConstants.NOTIFICATION_PAGE_TYPE_POLLING_INTERVAL).pipe(
map((() => {
const formIdToPageTypeMappingEntries = (() => {
const formIdToPageTypeMapping: Record<string, PageTypeOutput["pageType"]["type"]> = {
pm_login: "login",
pm_loginTwoFactor: "login2fa",
pm_loginUnlock: "unlock",
};
return Object.entries(formIdToPageTypeMapping);
})();
const loginUrl = `${entryUrl}/login`;

return () => {
const url = getLocationHref();
const pageType: PageTypeOutput["pageType"] = {url, type: "unknown"};

if (
!isLoggedIn()
&&
url === loginUrl
) {
for (const [formId, type] of formIdToPageTypeMappingEntries) {
const form = document.getElementById(formId);
const formVisible = form && form.offsetParent;

if (formVisible) {
pageType.type = type;
break;
}
}
}
Observable<TitleOutput>,
Observable<LoggedInOutput>,
Observable<PageTypeOutput>,
Observable<UnreadOutput>,
Observable<BatchEntityUpdatesCounterOutput>
] = [
new Observable<{ title: string }>((observer) => {
const titleEl = document.querySelector("title");

if (!titleEl) {
observer.next({title: `Failed to resolve "title" DOM element`});
observer.complete();
return;
}

return {pageType};
};
})()),
distinctUntilChanged(({pageType: prev}, {pageType: curr}) => curr.type === prev.type),
tap((value) => logger.verbose(JSON.stringify(value))),
),

(() => {
const responseListeners = [
{
re: new RegExp(`${entryApiUrl}/api/messages/count`),
handler: ({Counts}: { Counts?: Array<{ LabelID: string; Unread: number; }> }) => {
if (!Counts) {
return;
}
return Counts
.filter(({LabelID}) => LabelID === "0")
.reduce((accumulator, item) => accumulator + item.Unread, 0);
const mutation = new MutationObserver(() => {
observer.next({title: document.title});
});

mutation.observe(titleEl, {subtree: true, childList: true});

return {
unsubscribe: () => {
mutation.disconnect();
},
},
{
re: new RegExp(`${entryApiUrl}/api/events/.*==`),
handler: ({MessageCounts}: Rest.Model.EventResponse) => {
if (!MessageCounts) {
return;
};
}),

interval(WebviewConstants.NOTIFICATION_LOGGED_IN_POLLING_INTERVAL).pipe(
map(() => isLoggedIn()),
distinctUntilChanged(),
map((loggedIn) => ({loggedIn})),
),

// TODO listen for location.href change instead of starting polling interval
interval(WebviewConstants.NOTIFICATION_PAGE_TYPE_POLLING_INTERVAL).pipe(
map((() => {
const formIdToPageTypeMappingEntries = (() => {
const formIdToPageTypeMapping: Record<string, PageTypeOutput["pageType"]["type"]> = {
pm_login: "login",
pm_loginTwoFactor: "login2fa",
pm_loginUnlock: "unlock",
};
return Object.entries(formIdToPageTypeMapping);
})();
const loginUrl = `${entryUrl}/login`;

return () => {
const url = getLocationHref();
const pageType: PageTypeOutput["pageType"] = {url, type: "unknown"};

if (
!isLoggedIn()
&&
url === loginUrl
) {
for (const [formId, type] of formIdToPageTypeMappingEntries) {
const form = document.getElementById(formId);
const formVisible = form && form.offsetParent;

if (formVisible) {
pageType.type = type;
break;
}
}
}
return MessageCounts
.filter(({LabelID}) => LabelID === "0")
.reduce((accumulator, item) => accumulator + item.Unread, 0);

return {pageType};
};
})()),
distinctUntilChanged(({pageType: prev}, {pageType: curr}) => curr.type === prev.type),
tap((value) => logger.verbose(JSON.stringify(value))),
),

(() => {
const responseListeners = [
{
re: new RegExp(`${entryApiUrl}/api/messages/count`),
handler: ({Counts}: { Counts?: Array<{ LabelID: string; Unread: number; }> }) => {
if (!Counts) {
return;
}
return Counts
.filter(({LabelID}) => LabelID === "0")
.reduce((accumulator, item) => accumulator + item.Unread, 0);
},
},
},
];

return AJAX_SEND_NOTIFICATION$.pipe(
mergeMap((request) => responseListeners
.filter(({re}) => {
return re.test(request.responseURL);
})
.reduce(
(accumulator, {handler}) => {
const responseData = JSON.parse(request.responseText);
const value = (handler as any)(responseData);

return typeof value === "number"
? accumulator.concat([{unread: value}])
: accumulator;
{
re: new RegExp(`${entryApiUrl}/api/events/.*==`),
handler: ({MessageCounts}: Rest.Model.EventResponse) => {
if (!MessageCounts) {
return;
}
return MessageCounts
.filter(({LabelID}) => LabelID === "0")
.reduce((accumulator, item) => accumulator + item.Unread, 0);
},
[] as UnreadOutput[],
},
];

return AJAX_SEND_NOTIFICATION$.pipe(
mergeMap((request) => responseListeners
.filter(({re}) => {
return re.test(request.responseURL);
})
.reduce(
(accumulator, {handler}) => {
const responseData = JSON.parse(request.responseText);
const value = (handler as any)(responseData);

return typeof value === "number"
? accumulator.concat([{unread: value}])
: accumulator;
},
[] as UnreadOutput[],
)),
distinctUntilChanged((prev, curr) => curr.unread === prev.unread),
);
})(),

(() => {
const innerLogger = curryFunctionMembers(logger, `[entity update notification]`);
const eventsUrlRe = new RegExp(`${entryApiUrl}/api/events/.*==`);
const notification = {batchEntityUpdatesCounter: 0};
const notificationReceived$: Observable<Rest.Model.EventResponse> = AJAX_SEND_NOTIFICATION$.pipe(
filter((request) => eventsUrlRe.test(request.responseURL)),
map((request) => JSON.parse(request.responseText)),
);

return notificationReceived$.pipe(
buffer(notificationReceived$.pipe(
debounceTime(ONE_SECOND_MS * 1.5),
)),
distinctUntilChanged((prev, curr) => curr.unread === prev.unread),
);
})(),

(() => {
const innerLogger = curryFunctionMembers(logger, `[entity update notification]`);
const eventsUrlRe = new RegExp(`${entryApiUrl}/api/events/.*==`);
const notification = {batchEntityUpdatesCounter: 0};
const notificationReceived$: Observable<Rest.Model.EventResponse> = AJAX_SEND_NOTIFICATION$.pipe(
filter((request) => eventsUrlRe.test(request.responseURL)),
map((request) => JSON.parse(request.responseText)),
);

return notificationReceived$.pipe(
buffer(notificationReceived$.pipe(
debounceTime(ONE_SECOND_MS * 1.5),
)),
concatMap((events) => from(buildDbPatch({events, parentLogger: innerLogger}, true))),
concatMap((patch) => {
if (!isEntityUpdatesPatchNotEmpty(patch)) {
return EMPTY;
}
for (const key of (Object.keys(patch) as Array<keyof typeof patch>)) {
innerLogger.info(`upsert/remove ${key}: ${patch[key].upsert.length}/${patch[key].remove.length}`);
}
notification.batchEntityUpdatesCounter++;
return [notification];
}),
);
})(),
];
concatMap((events) => from(buildDbPatch({events, parentLogger: innerLogger}, true))),
concatMap((patch) => {
if (!isEntityUpdatesPatchNotEmpty(patch)) {
return EMPTY;
}
for (const key of (Object.keys(patch) as Array<keyof typeof patch>)) {
innerLogger.info(`upsert/remove ${key}: ${patch[key].upsert.length}/${patch[key].remove.length}`);
}
notification.batchEntityUpdatesCounter++;
return [notification];
}),
);
})(),
]
;

return merge(...observables);
},
Expand Down
7 changes: 7 additions & 0 deletions src/electron-preload/webview/tutanota/api/index.ts
Expand Up @@ -150,6 +150,7 @@ function bootstrapEndpoints(api: Unpacked<ReturnType<typeof resolveProviderApi>>

logger.info();

type TitleOutput = Required<Pick<TutanotaNotificationOutput, "title">>;
type LoggedInOutput = Required<Pick<TutanotaNotificationOutput, "loggedIn">>;
type PageTypeOutput = Required<Pick<TutanotaNotificationOutput, "pageType">>;
type UnreadOutput = Required<Pick<TutanotaNotificationOutput, "unread">>;
Expand All @@ -159,11 +160,17 @@ function bootstrapEndpoints(api: Unpacked<ReturnType<typeof resolveProviderApi>>
// so app reacts to the mails/folders updates instantly

const observables: [
Observable<TitleOutput>,
Observable<LoggedInOutput>,
Observable<PageTypeOutput>,
Observable<UnreadOutput>,
Observable<BatchEntityUpdatesCounterOutput>
] = [
new Observable<{ title: string }>((observer) => {
observer.next({title: `"title" DOM element listening is not enabled`});
observer.complete();
}),

interval(WebviewConstants.NOTIFICATION_LOGGED_IN_POLLING_INTERVAL).pipe(
map(() => isLoggedIn()),
distinctUntilChanged(),
Expand Down
1 change: 1 addition & 0 deletions src/shared/model/account.ts
Expand Up @@ -20,6 +20,7 @@ export type AccountConfigTutanota = GenericAccountConfig<"tutanota", "password"
export type AccountConfig<T extends AccountType = AccountType> = Extract<AccountConfigProtonmail | AccountConfigTutanota, { type: T }>;

export interface GenericNotifications<NotificationPageTypes extends string = string> {
title: string;
loggedIn: boolean;
unread: number;
pageType: { url?: string; type: NotificationPageTypes; };
Expand Down
2 changes: 2 additions & 0 deletions src/shared/model/options.ts
Expand Up @@ -50,6 +50,7 @@ export interface Config extends BaseConfig, Partial<StoreModel.StoreEntity> {
hideControls: boolean;
idleTimeLogOutSec: number;
logLevel: LogLevel;
reflectSelectedAccountTitle: boolean;
startMinimized: boolean;
unreadNotifications: boolean;
}
Expand All @@ -67,6 +68,7 @@ export type BaseConfig = Pick<Config,
| "hideControls"
| "idleTimeLogOutSec"
| "logLevel"
| "reflectSelectedAccountTitle"
| "startMinimized"
| "unreadNotifications">;

Expand Down
3 changes: 3 additions & 0 deletions src/shared/util.ts
Expand Up @@ -63,6 +63,7 @@ export function initialConfig(): Config {
hideControls: false,
idleTimeLogOutSec: 0,
logLevel: "error",
reflectSelectedAccountTitle: false,
startMinimized: true,
unreadNotifications: true,
};
Expand All @@ -83,6 +84,7 @@ export function pickBaseConfigProperties(
hideControls,
idleTimeLogOutSec,
logLevel,
reflectSelectedAccountTitle,
startMinimized,
unreadNotifications,
}: Config,
Expand All @@ -100,6 +102,7 @@ export function pickBaseConfigProperties(
hideControls,
idleTimeLogOutSec,
logLevel,
reflectSelectedAccountTitle,
startMinimized,
unreadNotifications,
};
Expand Down

0 comments on commit d862618

Please sign in to comment.