Skip to content

Commit c9b86cc

Browse files
committed
Expose firstMessageTimestamp, implement menu item to copy “mailbox permalink” Foundry376#1473
1 parent 7ade1c7 commit c9b86cc

File tree

8 files changed

+89
-21
lines changed

8 files changed

+89
-21
lines changed

app/internal_packages/thread-list/lib/thread-list-context-menu.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export default class ThreadListContextMenu {
4545
this.markAsReadItem(),
4646
this.markAsSpamItem(),
4747
this.starItem(),
48+
{ type: 'separator' },
49+
this.createMailboxLinkItem(),
4850
]);
4951
})
5052
.then(menuItems => {
@@ -243,6 +245,22 @@ export default class ThreadListContextMenu {
243245
};
244246
}
245247

248+
createMailboxLinkItem() {
249+
if (this.threadIds.length !== 1) {
250+
return null;
251+
}
252+
253+
return {
254+
label: localized('Copy mailbox permalink'),
255+
click: async () => {
256+
const id = this.threadIds[0];
257+
const thread = await DatabaseStore.findBy<Thread>(Thread, { id }).limit(1);
258+
if (!thread) return;
259+
require('electron').clipboard.writeText(thread.getMailboxPermalink());
260+
},
261+
};
262+
}
263+
246264
displayMenu() {
247265
const { remote } = require('electron');
248266
this.menuItemTemplate().then(template => {

app/internal_packages/thread-sharing/lib/main.tsx

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,51 @@ const DATE_EPSILON = 60; // Seconds
2727

2828
const _readFile = Promise.promisify(fs.readFile);
2929

30+
interface MailspringLinkParams {
31+
subject: string;
32+
lastDate?: number;
33+
date?: number;
34+
}
3035
const _parseOpenThreadUrl = mailspringUrlString => {
3136
const parsedUrl = url.parse(mailspringUrlString);
32-
const params: any = querystring.parse(parsedUrl.query);
33-
params.lastDate = parseInt(params.lastDate, 10);
34-
return params;
37+
const params = querystring.parse(parsedUrl.query) as any;
38+
return {
39+
subject: params.subject,
40+
date: params.date ? parseInt(params.date, 10) : undefined,
41+
lastDate: params.lastDate ? parseInt(params.lastDate, 10) : undefined,
42+
} as MailspringLinkParams;
3543
};
3644

37-
const _findCorrespondingThread = ({ subject, lastDate }, dateEpsilon = DATE_EPSILON) => {
45+
const _findCorrespondingThread = (
46+
{ subject, lastDate, date }: MailspringLinkParams,
47+
dateEpsilon = DATE_EPSILON
48+
) => {
49+
const dateClause = date
50+
? new Matcher.And([
51+
Thread.attributes.firstMessageTimestamp.lessThan(date + dateEpsilon),
52+
Thread.attributes.firstMessageTimestamp.greaterThan(date - dateEpsilon),
53+
])
54+
: new Matcher.Or([
55+
new Matcher.And([
56+
Thread.attributes.lastMessageSentTimestamp.lessThan(lastDate + dateEpsilon),
57+
Thread.attributes.lastMessageSentTimestamp.greaterThan(lastDate - dateEpsilon),
58+
]),
59+
new Matcher.And([
60+
Thread.attributes.lastMessageReceivedTimestamp.lessThan(lastDate + dateEpsilon),
61+
Thread.attributes.lastMessageReceivedTimestamp.greaterThan(lastDate - dateEpsilon),
62+
]),
63+
]);
64+
3865
return DatabaseStore.findBy<Thread>(Thread).where([
3966
Thread.attributes.subject.equal(subject),
40-
new Matcher.Or([
41-
new Matcher.And([
42-
Thread.attributes.lastMessageSentTimestamp.lessThan(lastDate + dateEpsilon),
43-
Thread.attributes.lastMessageSentTimestamp.greaterThan(lastDate - dateEpsilon),
44-
]),
45-
new Matcher.And([
46-
Thread.attributes.lastMessageReceivedTimestamp.lessThan(lastDate + dateEpsilon),
47-
Thread.attributes.lastMessageReceivedTimestamp.greaterThan(lastDate - dateEpsilon),
48-
]),
49-
]),
67+
dateClause,
5068
]);
5169
};
5270

5371
const _onOpenThreadFromWeb = (event, mailspringUrl) => {
54-
const { subject, lastDate } = _parseOpenThreadUrl(mailspringUrl);
72+
const params = _parseOpenThreadUrl(mailspringUrl);
5573

56-
_findCorrespondingThread({ subject, lastDate })
74+
_findCorrespondingThread(params)
5775
.then(thread => {
5876
if (!thread) {
5977
throw new Error('Thread not found');
@@ -62,7 +80,9 @@ const _onOpenThreadFromWeb = (event, mailspringUrl) => {
6280
})
6381
.catch(error => {
6482
AppEnv.reportError(error);
65-
AppEnv.showErrorDialog(localized(`The thread %@ does not exist in your mailbox!`, subject));
83+
AppEnv.showErrorDialog(
84+
localized(`The thread %@ does not exist in your mailbox!`, params.subject)
85+
);
6686
});
6787
};
6888

app/internal_packages/thread-sharing/lib/thread-sharing-button.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,25 @@ export default class ThreadSharingButton extends React.Component<{ items: any[];
2424
});
2525
};
2626

27+
_onCopyMailboxLink = () => {
28+
// Note: This is the mailbox link, not the thread sharing link, but they are managed together
29+
// since this plugin also implements the receiving side (_onOpenThreadFromWeb).
30+
require('electron').clipboard.writeText(this.props.thread.getMailboxPermalink());
31+
};
32+
2733
render() {
2834
if (this.props.items && this.props.items.length > 1) {
2935
return <span />;
3036
}
3137
const item = this.props.items[0];
3238

3339
return (
34-
<BindGlobalCommands commands={{ 'core:share-item-link': () => this._onClick() }}>
40+
<BindGlobalCommands
41+
commands={{
42+
'core:share-item-link': () => this._onClick(),
43+
'core:copy-mailbox-link': () => this._onCopyMailboxLink(),
44+
}}
45+
>
3546
<button
3647
className={`btn btn-toolbar thread-sharing-button ${isShared(item) && 'active'}`}
3748
title={localized('Share')}

app/keymaps/base.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
"core:snooze-item": "z",
2424
"core:print-thread": "mod+p",
25+
"core:copy-mailbox-link": "ctrl+l",
2526
"core:focus-item": "enter",
2627
"core:remove-from-view": ["backspace", "del"],
2728
"core:pop-sheet": "escape",

app/menus/darwin.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ module.exports = {
158158
},
159159
{ label: localized('Star'), command: 'core:star-item' },
160160
{ label: localized('Snooze') + '...', command: 'core:snooze-item' },
161-
{ label: localized('Share Link') + '...', command: 'core:share-item-link' },
161+
{ type: 'separator' },
162+
{ label: localized('Share this thread') + '...', command: 'core:share-item-link' },
163+
{ label: localized('Copy mailbox permalink'), command: 'core:copy-mailbox-link' },
162164
{ type: 'separator' },
163165
{ label: localized('Remove from view'), command: 'core:remove-from-view' },
164166
{ label: localized('Remove and show next'), command: 'core:remove-and-next' },

app/menus/linux.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ module.exports = {
129129
},
130130
{ label: localized('Star'), command: 'core:star-item' },
131131
{ label: localized('Snooze') + '...', command: 'core:snooze-item' },
132-
{ label: localized('Share Link') + '...', command: 'core:share-item-link' },
132+
{ type: 'separator' },
133+
{ label: localized('Share this thread') + '...', command: 'core:share-item-link' },
134+
{ label: localized('Copy mailbox permalink'), command: 'core:copy-mailbox-link' },
133135
{ type: 'separator' },
134136
{ label: localized('Remove from view'), command: 'core:remove-from-view' },
135137
{ label: localized('Remove and show next'), command: 'core:remove-and-next' },

app/menus/win32.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ module.exports = {
107107
},
108108
{ label: localized('Star'), command: 'core:star-item' },
109109
{ label: localized('Snooze') + '...', command: 'core:snooze-item' },
110-
{ label: localized('Share Link') + '...', command: 'core:share-item-link' },
110+
{ type: 'separator' },
111+
{ label: localized('Share this thread') + '...', command: 'core:share-item-link' },
112+
{ label: localized('Copy mailbox permalink'), command: 'core:copy-mailbox-link' },
111113
{ type: 'separator' },
112114
{ label: localized('Remove from view'), command: 'core:remove-from-view' },
113115
{ label: localized('Remove and show next'), command: 'core:remove-and-next' },

app/src/flux/models/thread.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ export class Thread extends ModelWithMetadata {
104104
modelKey: 'attachmentCount',
105105
}),
106106

107+
firstMessageTimestamp: Attributes.DateTime({
108+
queryable: true,
109+
jsonKey: 'fmt',
110+
modelKey: 'firstMessageTimestamp',
111+
}),
112+
107113
lastMessageReceivedTimestamp: Attributes.DateTime({
108114
queryable: true,
109115
jsonKey: 'lmrt',
@@ -139,6 +145,7 @@ export class Thread extends ModelWithMetadata {
139145
public labels: Label[];
140146
public participants: Contact[];
141147
public attachmentCount: number;
148+
public firstMessageTimestamp: Date;
142149
public lastMessageReceivedTimestamp: Date;
143150
public lastMessageSentTimestamp: Date;
144151
public inAllMail: boolean;
@@ -206,4 +213,9 @@ export class Thread extends ModelWithMetadata {
206213
}
207214
return out;
208215
}
216+
217+
getMailboxPermalink() {
218+
const subject = encodeURIComponent(this.subject);
219+
return `mailspring://thread?subject=${subject}&date=${this.firstMessageTimestamp.getTime()}`;
220+
}
209221
}

0 commit comments

Comments
 (0)