Skip to content

Commit

Permalink
enable "move all to" button, closes #276
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed Apr 11, 2020
1 parent e4e8ac5 commit 6b375ec
Show file tree
Hide file tree
Showing 25 changed files with 384 additions and 162 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "electron-mail",
"description": "Unofficial ProtonMail Desktop App",
"version": "4.4.3",
"version": "4.5.0",
"author": "Vladimir Yakovlev <desktop-app@protonmail.ch>",
"license": "MIT",
"homepage": "https://github.com/vladimiry/ElectronMail",
Expand Down
1 change: 1 addition & 0 deletions src/electron-main/__test__/database/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function buildFolder(): Folder {
name: randomstring.generate(),
folderType: MAIL_FOLDER_TYPE.SENT,
mailFolderId: "123",
exclusive: 234,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/electron-main/database/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const DATABASE_VERSION = "5";
export const DATABASE_VERSION = "6";

export const DB_INSTANCE_PROP_NAME = "dbInstance";
5 changes: 4 additions & 1 deletion src/electron-main/database/entity/folder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {IsIn, IsNotEmpty, IsString} from "class-validator";
import {IsIn, IsInt, IsNotEmpty, IsString} from "class-validator";

import * as Model from "src/shared/model/database";
import {Entity} from "./base";
Expand All @@ -13,4 +13,7 @@ export class Folder extends Entity implements Model.Folder {
@IsNotEmpty()
@IsString()
mailFolderId!: Model.Folder["mailFolderId"];

@IsInt()
exclusive!: Model.Folder["exclusive"];
}
1 change: 1 addition & 0 deletions src/electron-main/database/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const resolveAccountFolders: (
id as any, // eslint-disable-line @typescript-eslint/no-explicit-any
),
mailFolderId: id,
exclusive: 1,
}));

const result: typeof resolveAccountFolders = (account) => [
Expand Down
17 changes: 17 additions & 0 deletions src/electron-main/storage-upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,23 @@ export async function upgradeDatabase(db: Database, accounts: Settings["accounts
needToSave = true;
}

if (Number(db.getVersion()) < 6) {
for (const {account} of db) {
for (const [/* folderPk */, folder] of Object.entries(account.folders)) {
if (typeof folder.exclusive === "number") {
continue;
}
type RawFolder = Pick<import("src/electron-preload/webview/lib/rest-model/response-entity/folder").Label, "Exclusive">;
const rawRestResponseFolder: Readonly<RawFolder> = JSON.parse(folder.raw);
(folder as Mutable<Pick<typeof folder, "exclusive">>).exclusive = rawRestResponseFolder.Exclusive;
if (typeof folder.exclusive !== "number") {
throw new Error(`Failed to resolve "rawFolder.Exclusive" numeric property`);
}
needToSave = true;
}
}
}

// removing non existent accounts
await (async (): Promise<void> => {
const removePks: DbAccountPk[] = [];
Expand Down
1 change: 1 addition & 0 deletions src/electron-preload/webview/lib/database-entity/folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ export function buildFolder(input: RestModel.Label): Model.Folder {
folderType,
name,
mailFolderId: input.ID,
exclusive: input.Exclusive,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {NumberBoolean} from "src/electron-preload/webview/lib/rest-model/common"
export interface Label<TypeRecord = typeof LABEL_TYPE._.nameValueMap> extends Entity {
Color: string;
Display: NumberBoolean;
Exclusive: NumberBoolean;
Exclusive: number;
Name: string;
Notify: NumberBoolean;
Order: number;
Expand Down
14 changes: 12 additions & 2 deletions src/electron-preload/webview/primary/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ const endpoints: ProtonApi = {
}
},

async makeRead(input) {
_logger.info("makeRead()", input.zoneName);
async makeMailRead(input) {
_logger.info("makeMailRead()", input.zoneName);

const {message} = await resolveProviderApi();

Expand All @@ -126,6 +126,16 @@ const endpoints: ProtonApi = {
// TODO consider triggering the "refresh" action (clicking the "refresh" button action)
},

async setMailFolder(input) {
_logger.info("setMailFolder()", input.zoneName);

const {message} = await resolveProviderApi();

await message.label({LabelID: input.folderId, IDs: input.messageIds});

// TODO consider triggering the "refresh" action (clicking the "refresh" button action)
},

async fillLogin({login, zoneName}) {
const logger = curryFunctionMembers(_logger, "fillLogin()", zoneName);

Expand Down
3 changes: 2 additions & 1 deletion src/electron-preload/webview/primary/provider-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface ProviderApi {
params?: RestModel.QueryParams & { LabelID?: Unpacked<RestModel.Message["LabelIDs"]> },
) => Promise<RestModel.MessagesResponse>;
read: (params: { IDs: ReadonlyArray<RestModel.Message["ID"]> }) => Promise<void>;
label: (params: { LabelID: RestModel.Label["ID"]; IDs: ReadonlyArray<RestModel.Message["ID"]> }) => Promise<void>;
};
contact: {
get: (id: RestModel.Contact["ID"]) => Promise<RestModel.ContactResponse["Contact"]>;
Expand Down Expand Up @@ -190,7 +191,7 @@ export async function resolveProviderApi(): Promise<ProviderApi> {
}),
message: resolveService<ProviderApi["message"]>(injector, "messageApi", {
...rateLimiting,
rateLimitedMethodNames: ["get", "query"/*, "read"*/],
rateLimitedMethodNames: ["get", "query"/*, "read"*/ /*, "label"*/],
}),
contact: resolveService<ProviderApi["contact"]>(injector, "Contact", {
...rateLimiting,
Expand Down
9 changes: 6 additions & 3 deletions src/shared/api/webview/primary.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ActionType, ScanService, createWebViewApiService} from "electron-rpc-api";

import {DbAccountPk, FsDbAccount, Mail} from "src/shared/model/database";
import {DbAccountPk, Folder, FsDbAccount, Mail} from "src/shared/model/database";
import {LoginFieldContainer, MailPasswordFieldContainer, PasswordFieldContainer} from "src/shared/model/container";
import {Notifications} from "src/shared/model/account";
import {PACKAGE_NAME} from "src/shared/constants";
Expand All @@ -10,6 +10,7 @@ import {buildLoggerBundle} from "src/electron-preload/lib/util";

const {Promise, Observable} = ActionType;

// TODO drop "ZoneApiParameter" use
export const PROTONMAIL_IPC_WEBVIEW_API_DEFINITION = {
ping:
Promise<DeepReadonly<ZoneApiParameter>>(),
Expand All @@ -27,8 +28,10 @@ export const PROTONMAIL_IPC_WEBVIEW_API_DEFINITION = {
} & ZoneApiParameter>>(),
fetchSingleMail:
Promise<DeepReadonly<DbAccountPk & { mailPk: Mail["pk"] } & ZoneApiParameter>>(),
makeRead:
Promise<DeepReadonly<DbAccountPk & { messageIds: string[] } & ZoneApiParameter>>(),
makeMailRead:
Promise<DeepReadonly<DbAccountPk & { messageIds: Array<Mail["id"]> } & ZoneApiParameter>>(),
setMailFolder:
Promise<DeepReadonly<DbAccountPk & { folderId: Folder["id"]; messageIds: Array<Mail["id"]> } & ZoneApiParameter>>(),
notification:
Observable<DeepReadonly<{ entryUrl: string; entryApiUrl: string } & ZoneApiParameter>, ProtonNotificationOutput>(),
unlock:
Expand Down
1 change: 1 addition & 0 deletions src/shared/model/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface Folder extends Entity {
readonly folderType: Unpacked<typeof Constants.MAIL_FOLDER_TYPE._.values>;
readonly name: string;
readonly mailFolderId: string;
readonly exclusive: number;
}

export interface ConversationEntry extends Entity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,25 @@ export class AccountViewPrimaryComponent extends AccountViewAbstractComponent im
if (!account.makeReadMailParams) {
return;
}
const {messageIds, mailsBundleKey} = account.makeReadMailParams;
const {messageIds} = account.makeReadMailParams;
this.event.emit({
type: "action",
payload: ACCOUNTS_ACTIONS.MakeMailRead({account, webView, messageIds, mailsBundleKey}),
payload: ACCOUNTS_ACTIONS.MakeMailRead({account, webView, messageIds}),
});
}),
);

this.addSubscription(
this.account$.pipe(
distinctUntilChanged(({setMailFolderParams: prev}, {setMailFolderParams: curr}) => curr === prev),
).subscribe((account) => {
if (!account.setMailFolderParams) {
return;
}
const {folderId, messageIds} = account.setMailFolderParams;
this.event.emit({
type: "action",
payload: ACCOUNTS_ACTIONS.SetMailFolder({account, webView, folderId, messageIds}),
});
}),
);
Expand Down
168 changes: 88 additions & 80 deletions src/web/browser-window/app/_accounts/accounts.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,94 +167,102 @@ export class AccountsEffects {
),
),
this.api.webViewClient(webView, {finishPromise}).pipe(
mergeMap((webViewClient) => merge(
timer(0, ONE_MINUTE_MS * 5).pipe(
tap(() => logger.verbose(`triggered by: timer`)),
),
FIRE_SYNCING_ITERATION$.pipe(
filter((value) => value.login === login),
tap(() => logger.verbose(`triggered by: FIRE_SYNCING_ITERATION$`)),
// user might be moving emails from here to there while syncing/"buildDbPatch" cycle is in progress
// debounce call reduces 404 fetch errors as we don't trigger fetching until user got settled down
debounceTime(ONE_SECOND_MS * 3),
),
fromEvent(window, "online").pipe(
tap(() => logger.verbose(`triggered by: "window.online" event`)),
delay(ONE_SECOND_MS * 3),
),
).pipe(
debounceTime(ONE_SECOND_MS),
debounce(() => pingOnlineStatusEverySecond$),
debounce(() => notSyncingPing$),
concatMap(() => {
return from(
ipcMainClient("dbGetAccountMetadata")({login}),
);
}),
withLatestFrom(this.store.pipe(select(OptionsSelectors.CONFIG.timeouts))),
concatMap(([metadata, timeouts]) => {
const bootstrapping = !isDatabaseBootstrapped(metadata);
mergeMap((webViewClient) => {
const syncingIterationTrigger$: Observable<null> = merge(
timer(0, ONE_MINUTE_MS * 5).pipe(
tap(() => logger.verbose(`triggered by: timer`)),
map(() => null),
),
fromEvent(window, "online").pipe(
tap(() => logger.verbose(`triggered by: "window.online" event`)),
delay(ONE_SECOND_MS * 3),
map(() => null),
),
FIRE_SYNCING_ITERATION$.pipe(
filter((value) => value.login === login),
tap(() => logger.verbose(`triggered by: FIRE_SYNCING_ITERATION$`)),
// user might be moving emails from here to there while syncing/"buildDbPatch" cycle is in progress
// debounce call reduces 404 fetch errors as we don't trigger fetching until user got settled down
debounceTime(ONE_SECOND_MS * 3),
),
).pipe(
map(() => null),
);

if (bootstrapping && bootstrappingTriggeredOnce) {
return throwError(
new Error(`Database bootstrap fetch has already been called once for the account, ${zoneName}`),
return syncingIterationTrigger$.pipe(
debounceTime(ONE_SECOND_MS),
debounce(() => pingOnlineStatusEverySecond$),
debounce(() => notSyncingPing$),
concatMap(() => {
return from(
ipcMainClient("dbGetAccountMetadata")({login}),
);
}

const timeoutMs = bootstrapping
? timeouts.dbBootstrapping
: timeouts.dbSyncing;
}),
withLatestFrom(this.store.pipe(select(OptionsSelectors.CONFIG.timeouts))),
concatMap(([metadata, timeouts]) => {
const bootstrapping = !isDatabaseBootstrapped(metadata);

logger.verbose(
`calling "buildDbPatch" api`,
JSON.stringify({
timeoutMs,
bootstrapping,
bootstrappingTriggeredOnce,
}),
);
if (bootstrapping && bootstrappingTriggeredOnce) {
return throwError(
new Error(`Database bootstrap fetch has already been called once for the account, ${zoneName}`),
);
}

const timeoutMs = bootstrapping
? timeouts.dbBootstrapping
: timeouts.dbSyncing;

logger.verbose(
`calling "buildDbPatch" api`,
JSON.stringify({
timeoutMs,
bootstrapping,
bootstrappingTriggeredOnce,
}),
);

this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {syncing: true}}));
this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {syncing: true}}));

const result$ = from(
webViewClient("buildDbPatch", {timeoutMs})({
login,
zoneName,
metadata,
}),
).pipe(
concatMap(() => EMPTY),
takeUntil(
fromEvent(window, "offline").pipe(
tap(() => {
logger.verbose(`offline event`);

// tslint:disable-next-line:early-exit
if (bootstrapping && bootstrappingTriggeredOnce) {
bootstrappingTriggeredOnce = false;
logger.verbose(
[
`reset "bootstrappingTriggeredOnce" state as previous iteration got aborted`,
`by the "offline" event`,
].join(" "),
);
}
}),
const result$ = from(
webViewClient("buildDbPatch", {timeoutMs})({
login,
zoneName,
metadata,
}),
).pipe(
concatMap(() => of(ACCOUNTS_ACTIONS.Synced({pk: {login}}))),
takeUntil(
fromEvent(window, "offline").pipe(
tap(() => {
logger.verbose(`offline event`);

// tslint:disable-next-line:early-exit
if (bootstrapping && bootstrappingTriggeredOnce) {
bootstrappingTriggeredOnce = false;
logger.verbose(
[
`reset "bootstrappingTriggeredOnce" state as previous iteration`,
`got aborted by the "offline" event`,
].join(" "),
);
}
}),
),
),
),
finalize(() => {
return this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {syncing: false}}));
}),
);
finalize(() => {
this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {syncing: false}}));
}),
);

if (bootstrapping) {
bootstrappingTriggeredOnce = true;
logger.verbose("bootstrappingTriggeredOnce = true");
}
if (bootstrapping) {
bootstrappingTriggeredOnce = true;
logger.verbose("bootstrappingTriggeredOnce = true");
}

return result$;
}),
)),
return result$;
}),
);
}),
),
).pipe(
takeUntil(dispose$),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="d-flex flex-grow-1 type-{{folder.folderType}}">
<div class="d-flex flex-grow-1 type-{{folder.folderType}} folder-{{folder.exclusive > 0}}">
<i class="fa mr-2"></i>
<span class="mr-1">{{ folder.name }}</span>
<span *ngIf="folder.size">({{ folder.size }})</span>
Expand Down

0 comments on commit 6b375ec

Please sign in to comment.