mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-08 05:34:23 +08:00
Merge branch 'Foundry376:master' into eml-work-(export/import)
This commit is contained in:
commit
ebb6f50431
48 changed files with 1186 additions and 589 deletions
|
@ -56,7 +56,7 @@ environment:
|
|||
SIGN_BUILD: true
|
||||
WINDOWS_CODESIGN_CERT: .\app\build\resources\certs\win\win-codesigning.p12
|
||||
WINDOWS_CODESIGN_CERT_PASSWORD:
|
||||
secure: 3ddxqTBFv+xflIzypB0fNg==
|
||||
secure: RovLLdpfq3LDeWzXZ0DLD9NxbOBxIi2FI8yndjCH+Yg=
|
||||
encrypted_faf2708e46e2_key:
|
||||
secure: mdegN/AldrADhtEop6mDwq6d4jUskzijK2X7Twf2lj9t3jdaW4OtMuJ5Ywyt+GN/N7qMFr7LOvxQ5gz4aoIW+Dg9d03AX3BH1o4BI6g+wdk=
|
||||
encrypted_faf2708e46e2_iv:
|
||||
|
|
|
@ -2,7 +2,7 @@ version: 2
|
|||
jobs:
|
||||
test:
|
||||
docker:
|
||||
- image: circleci/node:12.9.1-stretch
|
||||
- image: cimg/python:3.9-node
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
|
|
|
@ -93,6 +93,7 @@ after_success:
|
|||
- timeout 180 sudo /snap/bin/transfer *.snap
|
||||
# Decrypt the snapcraft login information
|
||||
- openssl aes-256-cbc -K $encrypted_d506bd5213c4_key -iv $encrypted_d506bd5213c4_iv -in .snapcraft/credentials.enc -out .snapcraft/credentials -d
|
||||
- export SNAPCRAFT_STORE_CREDENTIALS=$(cat .snapcraft/credentials)
|
||||
|
||||
after_failure:
|
||||
- test "$TRAVIS_OS_NAME" = "linux" && sudo journalctl -u snapd || true
|
||||
|
@ -102,5 +103,5 @@ deploy:
|
|||
branch: master
|
||||
condition: $TRAVIS_OS_NAME = linux
|
||||
provider: script
|
||||
script: sudo snapcraft login --with .snapcraft/credentials && sudo snapcraft push *.snap --release edge || true
|
||||
script: sudo snapcraft push *.snap --release edge || true
|
||||
skip_cleanup: true
|
||||
|
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -1,5 +1,59 @@
|
|||
# Mailspring Changelog
|
||||
|
||||
## 1.10.8 (12/29/2022)
|
||||
|
||||
_Happy new year! Thanks for your continued support - we're celebrating five years of open-source and the 60th Mailspring release!_
|
||||
|
||||
Features:
|
||||
|
||||
- You can now drag-to-resize inline images in the composer! (Thanks @glenn2223!)
|
||||
|
||||
Bug Fixes:
|
||||
|
||||
- Launching Mailspring after a long time no longer causes the app to crash trying to show the vacuum UI. (Thankfully these changes do complete, so this error is recoverable.)
|
||||
|
||||
- Mailspring no longer shows "0pm" at 12pm / noon in some scenarios. (Thanks @glenn2223!)
|
||||
|
||||
- Updated Italian translations (thanks @andy00087!)
|
||||
|
||||
## 1.10.7 (11/21/2022)
|
||||
|
||||
- When creating a new IMAP + SMTP account, Mailspring sends a test message through the SMTP gateway to yourself, instead of attempting to send the message to an invalid address.
|
||||
|
||||
- When clicking "Unsubscribe", Mailspring verifies that the unsubscribe URL in the email will open in your default web browser.
|
||||
|
||||
- Composer recipient warnings are now optional. (Thanks @arhanjain!)
|
||||
|
||||
- The "and X more" display in message contacts no longer includes a stray `$` (Thanks @timdorr)
|
||||
|
||||
## 1.10.6 (10/10/2022)
|
||||
|
||||
Features:
|
||||
|
||||
- The send later delay can be skipped by clicking the new `Send now instead` button (localization required)
|
||||
|
||||
Changes:
|
||||
|
||||
- Mailsync now requests fewer emails at a time, fixing compatibility issues with large Office365 accounts. (Thanks @BrandonGillis for extensive testing of this change!)
|
||||
|
||||
- Inline image "cid:" references may appear only in `<img src=“”>`, and may not appear elsewhere in message bodies.
|
||||
|
||||
- Mailspring uses iframe sandboxing to disallow interactivity in message bodies, in addition to santizing loaded HTML down to a strict list of tags and attributes.
|
||||
|
||||
- Fixes a rare ResizeObserver error loop caused by messages resizing as they're unmounted.
|
||||
|
||||
- Mailspring's Flatpak version number has been updated.
|
||||
|
||||
- The Brazilian Portuguese translation has been reviewed and refined by @matheusreich (#2429)
|
||||
|
||||
- Fix the vertical overflow bug (Community - 3507) (Thanks @glenn2223! #2423)
|
||||
|
||||
- Fix `[Message Clipped - Show All]` link (Thanks @glenn2223! #2426)
|
||||
|
||||
- The message participant list is easier to interact with, thanks to several adjustments by @glenn2223! See https://github.com/Foundry376/Mailspring/pull/2425 for more details.
|
||||
|
||||
- The "Recent Emails" content in Mailspring's sidebar now displays the weekday and time (eg: "Mon, 10:15") and shows 5 days of emails rather than 2.
|
||||
|
||||
## 1.10.5 (8/10/2022)
|
||||
|
||||
Changes:
|
||||
|
|
|
@ -6,13 +6,7 @@ Providing localization in many languages is a challenge, and automatic translati
|
|||
|
||||
## Contributing Localizations
|
||||
|
||||
#### Option 1: Suggest Changes
|
||||
|
||||
From within Mailspring, choose "Developer > Toggle Localizer Tools" from the menu. A yellow bar appears at the bottom of the window. From here, you can use the small "Inspect" button to click text in the window which is untranslated or poorly translated. Type a new translation and click "Submit"!
|
||||
|
||||
_Note: These translations are reviewed manually and a Mailspring maintainer will change the necessary files._
|
||||
|
||||
#### Option 2: Submit a Pull Request
|
||||
#### Submit a Pull Request
|
||||
|
||||
If you have a GitHub account, you can improve the localization files directly and submit a Pull Request! If you're interested in providing many translations, or translating things like network error messages you may not ever see yourself, this is the best bet. It also means you'll be recognized as a Mailspring contributor and the Mailspring project will appear on your GitHub profile!
|
||||
|
||||
|
|
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Security
|
||||
|
||||
Mailspring takes the privacy of your data seriously. When you connect email accounts to the app, your email credentials are stored securely in your system keychain. Mailspring does not transmit, store or process your mail in the cloud. For more information about how we protect and handle your data, take a look at the [Privacy Policy](https://getmailspring.com/privacy-policy). We've written up a detailed [breakdown of the information](https://foundry376.zendesk.com/hc/en-us/articles/360002704911-What-data-does-Mailspring-collect-when-I-use-the-product-) that Mailspring stores in the cloud, and we allow you to [delete it all](https://foundry376.zendesk.com/hc/en-us/articles/115001994551-How-do-I-delete-my-Mailspring-ID-and-all-data-stored-on-Mailspring-servers-GDPR-) in compliance with GDPR.
|
||||
|
||||
When you set up the app you can also choose to skip "Mailspring ID" setup, which opts you out of our cloud features and prevents your data from being transmitted off your machine entirely.
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
If you have reason to believe that Mailspring is subject to a security vulnerability or breach, or believe a third-party or unofficial download site is hosting malware under the Mailspring name, please email our team at [security@foundry376.com](mailto:security@foundry376.com) with as much information as you can provide. You can expect a response within a day.
|
Binary file not shown.
|
@ -1,9 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<id><%= name %></id>
|
||||
<id>
|
||||
<%= name %>
|
||||
</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<name><%= productName %></name>
|
||||
<summary><%= description %></summary>
|
||||
<name>
|
||||
<%= productName %>
|
||||
</name>
|
||||
<summary>
|
||||
<%= description %>
|
||||
</summary>
|
||||
<description>
|
||||
<p>
|
||||
Mailspring is a new version of Nylas Mail maintained by one of the original authors. It's
|
||||
|
@ -23,7 +29,9 @@
|
|||
<url type="homepage">https://getmailspring.com/</url>
|
||||
<url type="bugtracker">https://github.com/Foundry376/Mailspring/issues</url>
|
||||
<url type="help">http://support.getmailspring.com/</url>
|
||||
<launchable type="desktop-id"><%= productName %>.desktop</launchable>
|
||||
<launchable type="desktop-id">
|
||||
<%= productName %>.desktop
|
||||
</launchable>
|
||||
|
||||
<developer_name>Mailspring</developer_name>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
|
@ -33,10 +41,19 @@
|
|||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="1.9.2" date="2021-09-06"/>
|
||||
<release version="1.9.1" date="2021-04-16"/>
|
||||
<release version="1.9.0" date="2021-04-14"/>
|
||||
<release version="1.8.0" date="2021-01-20"/>
|
||||
<release version="1.10.8" date="2022-12-29" />
|
||||
<release version="1.10.7" date="2022-11-21" />
|
||||
<release version="1.10.6" date="2022-08-10" />
|
||||
<release version="1.10.5" date="2022-08-10" />
|
||||
<release version="1.10.4" date="2022-08-09" />
|
||||
<release version="1.10.3" date="2022-04-18" />
|
||||
<release version="1.10.2" date="2022-03-29" />
|
||||
<release version="1.10.1" date="2022-03-12" />
|
||||
<release version="1.10.0" date="2022-02-28" />
|
||||
<release version="1.9.2" date="2021-09-06" />
|
||||
<release version="1.9.1" date="2021-04-16" />
|
||||
<release version="1.9.0" date="2021-04-14" />
|
||||
<release version="1.8.0" date="2021-01-20" />
|
||||
</releases>
|
||||
<content_rating type="oars-1.0" />
|
||||
</component>
|
||||
</component>
|
|
@ -342,7 +342,7 @@ export default class ComposerView extends React.Component<ComposerViewProps, Com
|
|||
});
|
||||
};
|
||||
|
||||
_isValidDraft = (options: { force?: boolean } = {}) => {
|
||||
_isValidDraft = (options: { forceRecipientWarnings?: boolean, forceMiscWarnings?: boolean } = {}) => {
|
||||
// We need to check the `DraftStore` because the `DraftStore` is
|
||||
// immediately and synchronously updated as soon as this function
|
||||
// fires. Since `setState` is asynchronous, if we used that as our only
|
||||
|
@ -353,31 +353,61 @@ export default class ComposerView extends React.Component<ComposerViewProps, Com
|
|||
|
||||
const dialog = require('@electron/remote').dialog;
|
||||
const { session } = this.props;
|
||||
const { errors, warnings } = session.validateDraftForSending();
|
||||
const { recipientErrors, recipientWarnings } = session.validateDraftRecipients();
|
||||
const { miscErrors, miscWarnings } = session.validateDraftForSending();
|
||||
|
||||
if (errors.length > 0) {
|
||||
// Display errors
|
||||
if (recipientErrors.length > 0) {
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: [localized('Edit Message'), localized('Cancel')],
|
||||
message: localized('Cannot send message'),
|
||||
detail: errors[0],
|
||||
detail: recipientErrors[0],
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (miscErrors.length > 0) {
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: [localized('Edit Message'), localized('Cancel')],
|
||||
message: localized('Cannot send message'),
|
||||
detail: miscErrors[0],
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (warnings.length > 0 && !options.force) {
|
||||
// Display warnings
|
||||
if (recipientWarnings.length > 0 && !options.forceRecipientWarnings) {
|
||||
const response = dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
buttons: [localized('Send Anyway'), localized('Send & Ignore Warnings For This Email'), localized('Cancel')],
|
||||
message: localized('Are you sure?'),
|
||||
detail: recipientWarnings.join('. '),
|
||||
});
|
||||
if (response === 0) {
|
||||
// response is button array index
|
||||
return this._isValidDraft({ forceRecipientWarnings: true, forceMiscWarnings: options.forceMiscWarnings });
|
||||
} else if (response === 1) {
|
||||
// Send & Ignore Future Warnings for Recipient Email
|
||||
session.addRecipientsToWarningBlacklist()
|
||||
return this._isValidDraft({ forceRecipientWarnings: true, forceMiscWarnings: options.forceMiscWarnings });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (miscWarnings.length > 0 && !options.forceMiscWarnings) {
|
||||
const response = dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
buttons: [localized('Send Anyway'), localized('Cancel')],
|
||||
message: localized('Are you sure?'),
|
||||
detail: warnings.join('. '),
|
||||
detail: miscWarnings.join('. '),
|
||||
});
|
||||
if (response === 0) {
|
||||
// response is button array index
|
||||
return this._isValidDraft({ force: true });
|
||||
return this._isValidDraft({ forceRecipientWarnings: options.forceRecipientWarnings, forceMiscWarnings: true });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import cheerio from 'cheerio';
|
||||
import {
|
||||
Message,
|
||||
ComponentRegistry,
|
||||
} from 'mailspring-exports';
|
||||
import { Message, ComponentRegistry } from 'mailspring-exports';
|
||||
import { UnsubscribeHeader } from './unsubscribe-header';
|
||||
|
||||
const regexps = [
|
||||
|
@ -60,18 +57,22 @@ const regexps = [
|
|||
/notisinställningar/gi,
|
||||
];
|
||||
|
||||
const _throwawayCache: Record<string, string> = {};
|
||||
|
||||
interface UnsubscribeAction {
|
||||
href: string;
|
||||
innerText: string;
|
||||
}
|
||||
|
||||
function bestUnsubscribeLink(message): string {
|
||||
function bestUnsubscribeLink(message: Message): string {
|
||||
if (_throwawayCache[message.id] !== undefined) {
|
||||
return _throwawayCache[message.id];
|
||||
}
|
||||
|
||||
let result = null;
|
||||
|
||||
// Only check the body if it has been downloaded already
|
||||
if (message.body) {
|
||||
|
||||
const dom = cheerio.load(message.body);
|
||||
const links = _getLinks(dom);
|
||||
|
||||
|
@ -80,6 +81,9 @@ function bestUnsubscribeLink(message): string {
|
|||
if (re.test(link.href)) {
|
||||
// If the URL contains e.g. "unsubscribe" we assume that we have correctly
|
||||
// detected the unsubscribe link.
|
||||
|
||||
_throwawayCache[message.id] = link.href;
|
||||
|
||||
return link.href;
|
||||
}
|
||||
if (re.test(link.innerText)) {
|
||||
|
@ -90,13 +94,15 @@ function bestUnsubscribeLink(message): string {
|
|||
}
|
||||
}
|
||||
|
||||
_throwawayCache[message.id] = result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const UnsubscribeHeaderContainer: React.FunctionComponent<{ message: Message }> = ({ message }) => {
|
||||
const unsubscribeAction = bestUnsubscribeLink(message)
|
||||
const unsubscribeAction = bestUnsubscribeLink(message);
|
||||
|
||||
return unsubscribeAction ? <UnsubscribeHeader unsubscribeAction={unsubscribeAction} /> : null;
|
||||
};
|
||||
|
||||
|
@ -116,24 +122,24 @@ export function deactivate() {
|
|||
// Returns a list of links as {href, innerText} objects
|
||||
function _getLinks($): UnsubscribeAction[] {
|
||||
const aParents = [];
|
||||
$('a').each((index, aTag) => {
|
||||
$('a:not(blockquote a)').each((_index, aTag) => {
|
||||
if (aTag && aTag.parent && !$(aParents).is(aTag.parent)) {
|
||||
aParents.unshift(aTag.parent);
|
||||
}
|
||||
});
|
||||
|
||||
const links = [];
|
||||
$(aParents).each((parentIndex, parent) => {
|
||||
$(aParents).each((_parentIndex, parent) => {
|
||||
let link = false;
|
||||
let leftoverText = "";
|
||||
$(parent.children).each((childIndex, child) => {
|
||||
let leftoverText = '';
|
||||
$(parent.children).each((_childIndex, child) => {
|
||||
if ($(child).is($('a'))) {
|
||||
if (link !== false && leftoverText.length > 0) {
|
||||
links.push({
|
||||
href: link,
|
||||
innerText: leftoverText,
|
||||
});
|
||||
leftoverText = "";
|
||||
leftoverText = '';
|
||||
}
|
||||
link = $(child).attr('href');
|
||||
}
|
||||
|
@ -142,7 +148,7 @@ function _getLinks($): UnsubscribeAction[] {
|
|||
if (re.test(text)) {
|
||||
const splitup = text.split(re);
|
||||
for (let i = 0; i < splitup.length; i += 1) {
|
||||
if (splitup[i] !== "" && splitup[i] !== undefined) {
|
||||
if (splitup[i] !== '' && splitup[i] !== undefined) {
|
||||
if (link !== false) {
|
||||
const fullLine = leftoverText + splitup[i];
|
||||
links.push({
|
||||
|
@ -150,7 +156,7 @@ function _getLinks($): UnsubscribeAction[] {
|
|||
innerText: fullLine,
|
||||
});
|
||||
link = false;
|
||||
leftoverText = "";
|
||||
leftoverText = '';
|
||||
} else {
|
||||
leftoverText += splitup[i];
|
||||
}
|
||||
|
@ -159,7 +165,7 @@ function _getLinks($): UnsubscribeAction[] {
|
|||
} else {
|
||||
leftoverText += text;
|
||||
}
|
||||
leftoverText += " ";
|
||||
leftoverText += ' ';
|
||||
});
|
||||
if (link !== false && leftoverText.length > 0) {
|
||||
links.push({
|
||||
|
@ -169,4 +175,4 @@ function _getLinks($): UnsubscribeAction[] {
|
|||
}
|
||||
});
|
||||
return links;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React from 'react';
|
||||
const { shell } = require('electron')
|
||||
import {
|
||||
localized,
|
||||
} from 'mailspring-exports';
|
||||
const { shell } = require('electron');
|
||||
import { localized } from 'mailspring-exports';
|
||||
|
||||
interface UnsubscribeHeaderProps {
|
||||
unsubscribeAction: string;
|
||||
|
@ -19,14 +17,17 @@ export class UnsubscribeHeader extends React.Component<UnsubscribeHeaderProps> {
|
|||
render() {
|
||||
const { unsubscribeAction } = this.props;
|
||||
return (
|
||||
<a className="unsubscribe-action" onClick={() => this._unsubscribe(unsubscribeAction)}>{localized('Unsubscribe')}</a>
|
||||
<a className="unsubscribe-action" onClick={() => this._unsubscribe(unsubscribeAction)}>
|
||||
{localized('Unsubscribe')}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
private _unsubscribe(url: string) {
|
||||
shell.openExternal(url);
|
||||
if (/^https?:\/\/.+/i.test(url)) {
|
||||
shell.openExternal(url);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default UnsubscribeHeader;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export enum CalendarView {
|
||||
DAY = 'day',
|
||||
WEEK = 'week',
|
||||
MONTH = 'month',
|
||||
DAY = 'Day',
|
||||
WEEK = 'Week',
|
||||
MONTH = 'Month',
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import React from 'react';
|
||||
import { Utils } from 'mailspring-exports';
|
||||
import { RetinaImg } from 'mailspring-component-kit';
|
||||
import { CalendarView } from './calendar-constants';
|
||||
|
||||
export class HeaderControls extends React.Component<{
|
||||
title: string;
|
||||
nextAction: () => void;
|
||||
prevAction: () => void;
|
||||
onChangeView: (view: CalendarView) => void;
|
||||
disabledViewButton: string;
|
||||
}> {
|
||||
static displayName = 'HeaderControls';
|
||||
|
||||
|
@ -35,6 +38,10 @@ export class HeaderControls extends React.Component<{
|
|||
);
|
||||
}
|
||||
|
||||
_changeView = newView => {
|
||||
this.props.onChangeView(newView);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="header-controls">
|
||||
|
@ -43,6 +50,28 @@ export class HeaderControls extends React.Component<{
|
|||
<span className="title">{this.props.title}</span>
|
||||
{this._renderNextAction()}
|
||||
</div>
|
||||
<div className="view-controls">
|
||||
{[
|
||||
//{view: CalendarView.DAY, isDisabled: CalendarView.DAY === this.props.disabledViewButton,},
|
||||
{
|
||||
view: CalendarView.WEEK,
|
||||
isDisabled: CalendarView.WEEK === this.props.disabledViewButton,
|
||||
},
|
||||
{
|
||||
view: CalendarView.MONTH,
|
||||
isDisabled: CalendarView.MONTH === this.props.disabledViewButton,
|
||||
},
|
||||
].map(buttonOptions => (
|
||||
<button
|
||||
key={buttonOptions.view}
|
||||
className={buttonOptions.isDisabled ? 'cur-view-btn' : 'view-btn'}
|
||||
onClick={() => this._changeView(buttonOptions.view)}
|
||||
disabled={buttonOptions.isDisabled}
|
||||
>
|
||||
{buttonOptions.view}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -26,7 +26,6 @@ import { Disposable } from 'rx-core';
|
|||
import { CalendarEventArgs } from './calendar-event-container';
|
||||
import { CalendarEventPopover } from './calendar-event-popover';
|
||||
|
||||
|
||||
const DISABLED_CALENDARS = 'mailspring.disabledCalendars';
|
||||
|
||||
const VIEWS = {
|
||||
|
@ -56,7 +55,7 @@ export interface MailspringCalendarViewProps extends EventRendererProps {
|
|||
/*
|
||||
* Mailspring Calendar
|
||||
*/
|
||||
interface MailspringCalendarProps { }
|
||||
interface MailspringCalendarProps {}
|
||||
|
||||
interface MailspringCalendarState {
|
||||
view: CalendarView;
|
||||
|
@ -197,9 +196,9 @@ export class MailspringCalendar extends React.Component<
|
|||
}
|
||||
};
|
||||
|
||||
_onCalendarMouseDown = () => { };
|
||||
_onCalendarMouseMove = () => { };
|
||||
_onCalendarMouseUp = () => { };
|
||||
_onCalendarMouseDown = () => {};
|
||||
_onCalendarMouseMove = () => {};
|
||||
_onCalendarMouseUp = () => {};
|
||||
|
||||
render() {
|
||||
const CurrentView = VIEWS[this.state.view];
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react';
|
||||
import { MailspringCalendarViewProps } from './mailspring-calendar';
|
||||
import { CalendarEventContainer } from './calendar-event-container';
|
||||
import { ScrollRegion, InjectedComponentSet } from 'mailspring-component-kit';
|
||||
import { CalendarView } from './calendar-constants';
|
||||
import { HeaderControls } from './header-controls';
|
||||
|
||||
export class MonthView extends React.Component<MailspringCalendarViewProps> {
|
||||
static displayName = 'MonthView';
|
||||
|
@ -10,6 +13,35 @@ export class MonthView extends React.Component<MailspringCalendarViewProps> {
|
|||
};
|
||||
|
||||
render() {
|
||||
return <button onClick={this._onClick}>Change to week</button>;
|
||||
return (
|
||||
<div className="calendar-view month-view">
|
||||
<CalendarEventContainer
|
||||
onCalendarMouseUp={this.props.onCalendarMouseUp}
|
||||
onCalendarMouseDown={this.props.onCalendarMouseDown}
|
||||
onCalendarMouseMove={this.props.onCalendarMouseMove}
|
||||
>
|
||||
<div className="top-banner">
|
||||
<InjectedComponentSet matching={{ role: 'Calendar:Week:Banner' }} direction="row" />
|
||||
</div>
|
||||
|
||||
<HeaderControls
|
||||
title={'Test 2022'}
|
||||
nextAction={() => console.log('Next Month')}
|
||||
prevAction={() => console.log('Previous Month')}
|
||||
onChangeView={this.props.onChangeView}
|
||||
disabledViewButton={CalendarView.MONTH}
|
||||
>
|
||||
<button
|
||||
key="today"
|
||||
className="btn"
|
||||
onClick={() => console.log('Go to today (Month)')}
|
||||
style={{ position: 'absolute', left: 10 }}
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
</HeaderControls>
|
||||
</CalendarEventContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { WeekViewAllDayEvents } from './week-view-all-day-events';
|
|||
import { CalendarEventContainer } from './calendar-event-container';
|
||||
import { CurrentTimeIndicator } from './current-time-indicator';
|
||||
import { Disposable } from 'rx-core';
|
||||
import { CalendarView } from './calendar-constants';
|
||||
import {
|
||||
overlapForEvents,
|
||||
maxConcurrentEvents,
|
||||
|
@ -264,6 +265,8 @@ export class WeekView extends React.Component<
|
|||
title={headerText}
|
||||
nextAction={this._onClickNextWeek}
|
||||
prevAction={this._onClickPrevWeek}
|
||||
onChangeView={this.props.onChangeView}
|
||||
disabledViewButton={CalendarView.WEEK}
|
||||
>
|
||||
<button
|
||||
key="today"
|
||||
|
|
|
@ -342,6 +342,38 @@ body.platform-win32 {
|
|||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.view-controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
border-radius: @border-radius-base;
|
||||
box-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.15), 0 -0.5px 0 rgba(0, 0, 0, 0.15),
|
||||
0.5px 0 0 rgba(0, 0, 0, 0.15), -0.5px 0 0 rgba(0, 0, 0, 0.15),
|
||||
0 0.5px 1px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
|
||||
.view-btn {
|
||||
width: 60px;
|
||||
border: 0;
|
||||
cursor: default;
|
||||
display: inline-block;
|
||||
color: @btn-default-text-color;
|
||||
background: @background-primary;
|
||||
|
||||
&:hover {
|
||||
background: darken(@background-primary, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.cur-view-btn {
|
||||
width: 60px;
|
||||
border: 0;
|
||||
cursor: default;
|
||||
display: inline-block;
|
||||
color: @text-color-inverse;
|
||||
background: @accent-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.center-controls {
|
||||
|
|
|
@ -121,7 +121,15 @@ export default class EmailFrame extends React.Component<EmailFrameProps> {
|
|||
// so it can attach event listeners again.
|
||||
this._lastFitSize = '';
|
||||
this._iframeComponent.didReplaceDocument();
|
||||
this._iframeDocObserver.observe(iframeEl.contentDocument.firstElementChild);
|
||||
|
||||
// Observe the <html> element within the iFrame for changes to it's content
|
||||
// size. We need to disconnect the observer before the HTML element is deleted
|
||||
// or Chrome gets into a "maximum call depth" Observer error.
|
||||
const observedEl = iframeEl.contentDocument.firstElementChild;
|
||||
this._iframeDocObserver.observe(observedEl);
|
||||
iframeEl.contentWindow.addEventListener('beforeunload', () => {
|
||||
this._iframeDocObserver.disconnect();
|
||||
});
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
Autolink(doc.body, {
|
||||
|
@ -196,6 +204,7 @@ export default class EmailFrame extends React.Component<EmailFrameProps> {
|
|||
>
|
||||
<EventedIFrame
|
||||
searchable
|
||||
sandbox="allow-forms allow-same-origin"
|
||||
seamless={true}
|
||||
style={{ height: 0 }}
|
||||
ref={cm => {
|
||||
|
|
|
@ -13,10 +13,14 @@ import {
|
|||
import { InjectedComponentSet, RetinaImg } from 'mailspring-component-kit';
|
||||
|
||||
import EmailFrame from './email-frame';
|
||||
import { BrowserWindow } from '@electron/remote';
|
||||
|
||||
const TransparentPixel =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII=';
|
||||
|
||||
const SpinnerImg =
|
||||
'<img alt="spinner.gif" src="mailspring://message-list/assets/spinner.gif" style="-webkit-user-drag: none;">';
|
||||
|
||||
class ConditionalQuotedTextControl extends React.Component<{ body: string; onClick?: () => void }> {
|
||||
static displayName = 'ConditionalQuotedTextControl';
|
||||
|
||||
|
@ -116,9 +120,11 @@ export default class MessageItemBody extends React.Component<
|
|||
|
||||
_onShowClipped = async () => {
|
||||
const { message } = this.props;
|
||||
const filepath = require('path').join(require('@electron/remote').app.getPath('temp'), `${message.id}.html`);
|
||||
const filepath = require('path').join(
|
||||
require('@electron/remote').app.getPath('temp'),
|
||||
`${message.id}.html`
|
||||
);
|
||||
fs.writeFileSync(filepath, message.body);
|
||||
const { BrowserWindow } = require('electron');
|
||||
const win = new BrowserWindow({
|
||||
title: `${message.subject}`,
|
||||
width: 800,
|
||||
|
@ -131,7 +137,7 @@ export default class MessageItemBody extends React.Component<
|
|||
win.loadURL(`file://${filepath}`);
|
||||
};
|
||||
|
||||
_mergeBodyWithFiles(body) {
|
||||
_mergeBodyWithFiles(body: string) {
|
||||
let merged = body;
|
||||
|
||||
// Replace cid: references with the paths to downloaded files
|
||||
|
@ -143,21 +149,18 @@ export default class MessageItemBody extends React.Component<
|
|||
|
||||
// Note: I don't like doing this with RegExp before the body is inserted into
|
||||
// the DOM, but we want to avoid "could not load cid://" in the console.
|
||||
const inlineImgRegexp = new RegExp(
|
||||
`<\\s*img[^>/]*src=['"]cid:${safeContentId}['"][^>]*>`,
|
||||
'gi'
|
||||
);
|
||||
|
||||
if (download && download.state !== 'finished') {
|
||||
const inlineImgRegexp = new RegExp(
|
||||
`<\\s*img.*src=['"]cid:${safeContentId}['"][^>]*>`,
|
||||
'gi'
|
||||
);
|
||||
// Render a spinner
|
||||
merged = merged.replace(
|
||||
inlineImgRegexp,
|
||||
() =>
|
||||
'<img alt="spinner.gif" src="mailspring://message-list/assets/spinner.gif" style="-webkit-user-drag: none;">'
|
||||
);
|
||||
merged = merged.replace(inlineImgRegexp, () => SpinnerImg);
|
||||
} else {
|
||||
const cidRegexp = new RegExp(`cid:${safeContentId}(@[^'"]+)?`, 'gi');
|
||||
merged = merged.replace(cidRegexp, `file://${AttachmentStore.pathForFile(file)}`);
|
||||
merged = merged.replace(inlineImgRegexp, match =>
|
||||
match.replace(`cid:${file.contentId}`, `file://${AttachmentStore.pathForFile(file)}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ interface MessageListState {
|
|||
minified: boolean;
|
||||
}
|
||||
|
||||
const { Menu, MenuItem } = require('@electron/remote');
|
||||
const PREF_REPLY_TYPE = 'core.sending.defaultReplyType';
|
||||
const PREF_RESTRICT_WIDTH = 'core.reading.restrictMaxWidth';
|
||||
const PREF_DESCENDING_ORDER = 'core.reading.descendingOrderMessageList';
|
||||
|
@ -351,7 +352,9 @@ class MessageList extends React.Component<Record<string, unknown>, MessageListSt
|
|||
<div className="message-subject-wrap">
|
||||
<MailImportantIcon thread={this.state.currentThread} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<span className="message-subject">{subject}</span>
|
||||
<span className="message-subject" onContextMenu={() => _onSubjectContextMenu()}>
|
||||
{subject}
|
||||
</span>
|
||||
<MailLabelSet
|
||||
removable
|
||||
includeCurrentCategories
|
||||
|
@ -369,6 +372,14 @@ class MessageList extends React.Component<Record<string, unknown>, MessageListSt
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
function _onSubjectContextMenu() {
|
||||
if (window.getSelection()?.type == 'Range') {
|
||||
const menu = new Menu();
|
||||
menu.append(new MenuItem({ role: 'copy' }));
|
||||
menu.popup({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_renderMinifiedBundle(bundle) {
|
||||
|
|
|
@ -3,7 +3,6 @@ import classnames from 'classnames';
|
|||
import React from 'react';
|
||||
import { localized, Actions, Contact } from 'mailspring-exports';
|
||||
|
||||
|
||||
const { Menu, MenuItem } = require('@electron/remote');
|
||||
const MAX_COLLAPSED = 5;
|
||||
|
||||
|
@ -34,18 +33,25 @@ export default class MessageParticipants extends React.Component<MessageParticip
|
|||
}
|
||||
|
||||
_shortNames(contacts = [], max = MAX_COLLAPSED) {
|
||||
let names = contacts.map(c =>
|
||||
c.displayName({
|
||||
includeAccountLabel: true,
|
||||
compact: !AppEnv.config.get('core.reading.detailedNames'),
|
||||
})
|
||||
);
|
||||
let names = contacts.map((c, i) => (
|
||||
<span key={`contact-${i}`}>
|
||||
{i > 0 && ', '}
|
||||
<span onContextMenu={() => this._onContactContextMenu(c)}>
|
||||
{c.displayName({
|
||||
includeAccountLabel: true,
|
||||
compact: !AppEnv.config.get('core.reading.detailedNames'),
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
));
|
||||
|
||||
if (names.length > max) {
|
||||
const extra = names.length - max;
|
||||
names = names.slice(0, max);
|
||||
names.push(`and ${extra} more`);
|
||||
names.push(<span key="contact-more"> and {extra} more</span>);
|
||||
}
|
||||
return names.join(', ');
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
_onSelectText = e => {
|
||||
|
@ -63,10 +69,17 @@ export default class MessageParticipants extends React.Component<MessageParticip
|
|||
|
||||
_onContactContextMenu = contact => {
|
||||
const menu = new Menu();
|
||||
menu.append(new MenuItem({ role: 'copy' }));
|
||||
menu.append(
|
||||
window.getSelection()?.type == 'Range'
|
||||
? new MenuItem({ role: 'copy' })
|
||||
: new MenuItem({
|
||||
label: `${localized(`Copy`)} "${contact.email}"`,
|
||||
click: () => navigator.clipboard.writeText(contact.email),
|
||||
})
|
||||
);
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: `${localized(`Email`)} ${contact.email}`,
|
||||
label: `${localized(`Email`)} ${contact.name ?? contact.email}`,
|
||||
click: () => Actions.composeNewDraftToRecipient(contact),
|
||||
})
|
||||
);
|
||||
|
@ -83,7 +96,11 @@ export default class MessageParticipants extends React.Component<MessageParticip
|
|||
if (c.name && c.name.length > 0 && c.name !== c.email) {
|
||||
return (
|
||||
<div key={`${c.email}-${i}`} className="participant selectable">
|
||||
<div className="participant-primary" onClick={this._onSelectText}>
|
||||
<div
|
||||
className="participant-primary"
|
||||
onClick={this._onSelectText}
|
||||
onContextMenu={() => this._onContactContextMenu(c)}
|
||||
>
|
||||
{c.fullName()}
|
||||
</div>
|
||||
<div className="participant-secondary">
|
||||
|
|
|
@ -237,6 +237,7 @@ body.platform-win32 {
|
|||
}
|
||||
.thread-injected-mail-labels {
|
||||
vertical-align: top;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.message-list-headers {
|
||||
|
|
|
@ -81,15 +81,15 @@ export default class ViewOnGithubButton extends React.Component<
|
|||
* `Store`. Since most of our React Components are registered into
|
||||
* {ComponentRegistry} regions instead of manually rendered top-down much
|
||||
* of our data is side-loaded from stores instead of passed in as props.
|
||||
*/
|
||||
*/
|
||||
componentDidMount() {
|
||||
/*
|
||||
* The `listen` method of {MailspringStore}s (which {GithubStore}
|
||||
* subclasses) returns an "unlistener" function. When the unlistener is
|
||||
* invoked (as it is in `componentWillUnmount`) the listener references
|
||||
* are cleaned up. Every time the `GithubStore` calls its `trigger`
|
||||
* method, the `_onStoreChanged` callback will be fired.
|
||||
*/
|
||||
* The `listen` method of {MailspringStore}s (which {GithubStore}
|
||||
* subclasses) returns an "unlistener" function. When the unlistener is
|
||||
* invoked (as it is in `componentWillUnmount`) the listener references
|
||||
* are cleaned up. Every time the `GithubStore` calls its `trigger`
|
||||
* method, the `_onStoreChanged` callback will be fired.
|
||||
*/
|
||||
this._unlisten = GithubStore.listen(this._onStoreChanged);
|
||||
}
|
||||
|
||||
|
@ -117,10 +117,10 @@ export default class ViewOnGithubButton extends React.Component<
|
|||
};
|
||||
|
||||
/*
|
||||
* getStateFromStores fetches the data the view needs from the
|
||||
* appropriate data source (our GithubStore). We return a basic object
|
||||
* that can be passed directly into `setState`.
|
||||
*/
|
||||
* getStateFromStores fetches the data the view needs from the
|
||||
* appropriate data source (our GithubStore). We return a basic object
|
||||
* that can be passed directly into `setState`.
|
||||
*/
|
||||
_getStateFromStores() {
|
||||
return {
|
||||
link: GithubStore.link(),
|
||||
|
@ -141,7 +141,7 @@ export default class ViewOnGithubButton extends React.Component<
|
|||
* request.
|
||||
*/
|
||||
_openLink = () => {
|
||||
if (this.state.link) {
|
||||
if (this.state.link && /^https?:\/\/.+/i.test(this.state.link)) {
|
||||
shell.openExternal(this.state.link);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -27,6 +27,10 @@ class PreferencesGeneral extends React.Component<{
|
|||
app.quit();
|
||||
};
|
||||
|
||||
_onResetEmailsThatIgnoreWarnings = () => {
|
||||
localStorage.removeItem("recipientWarningBlacklist");
|
||||
}
|
||||
|
||||
_onResetAccountsAndSettings = () => {
|
||||
const chosen = require('@electron/remote').dialog.showMessageBoxSync({
|
||||
type: 'info',
|
||||
|
@ -55,6 +59,7 @@ class PreferencesGeneral extends React.Component<{
|
|||
ipc.send('command', 'application:reset-database', {});
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="container-general">
|
||||
|
@ -76,6 +81,9 @@ class PreferencesGeneral extends React.Component<{
|
|||
<div className="two-columns-flexbox" style={{ paddingTop: 30 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<SendingSection config={this.props.config} configSchema={this.props.configSchema} />
|
||||
<div className="btn" onClick={this._onResetEmailsThatIgnoreWarnings} style={{ marginLeft: 0, marginTop:5 }}>
|
||||
{localized('Reset Emails that Ignore Warnings')}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: 30 }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
|
|
|
@ -21,12 +21,18 @@ class ThreadListVertical extends React.Component<
|
|||
handle={ResizableHandle.Bottom}
|
||||
onResize={h => this._onResize(h)}
|
||||
>
|
||||
<InjectedComponentSet
|
||||
matching={{ role: 'ThreadList' }}
|
||||
/>
|
||||
<InjectedComponentSet matching={{ role: 'ThreadList' }} />
|
||||
</ResizableRegion>
|
||||
<ResizableRegion>
|
||||
<div style={{ height: '100%', width: '100%', borderTop: '0.5px solid #dddddd' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
borderTop: '0.5px solid #dddddd',
|
||||
}}
|
||||
>
|
||||
<div className="sheet-toolbar" style={{ borderBottom: '0' }}>
|
||||
<InjectedComponentSet
|
||||
matching={{
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import { localized, UndoRedoStore, SyncbackMetadataTask } from 'mailspring-exports';
|
||||
import { localized, UndoRedoStore, SyncbackMetadataTask, DatabaseStore, Message, Actions } from 'mailspring-exports';
|
||||
import { RetinaImg } from 'mailspring-component-kit';
|
||||
import { CSSTransitionGroup } from 'react-transition-group';
|
||||
import { PLUGIN_ID } from '../../../internal_packages/send-later/lib/send-later-constants';
|
||||
|
||||
function isUndoSend(block) {
|
||||
return (
|
||||
|
@ -11,6 +12,29 @@ function isUndoSend(block) {
|
|||
);
|
||||
}
|
||||
|
||||
async function sendMessageNow(block) {
|
||||
if (isUndoSend(block)) {
|
||||
const message = await DatabaseStore.find<Message>(Message, (block.tasks[0] as SyncbackMetadataTask).modelId),
|
||||
newExpiry = Math.floor(Date.now() / 1000);
|
||||
|
||||
Actions.queueTask(
|
||||
SyncbackMetadataTask.forSaving({
|
||||
model: message,
|
||||
pluginId: PLUGIN_ID,
|
||||
value: {
|
||||
expiration: newExpiry,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
block.tasks[0].value.expiration = newExpiry;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getUndoSendExpiration(block) {
|
||||
return block.tasks[0].value.expiration * 1000;
|
||||
}
|
||||
|
@ -75,6 +99,10 @@ const UndoSendContent = ({ block, onMouseEnter, onMouseLeave }) => {
|
|||
<div className="content" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Countdown expiration={getUndoSendExpiration(block)} />
|
||||
<div className="message">{localized('Sending soon...')}</div>
|
||||
<div className="action" onClick={async () => { await sendMessageNow(block) && onMouseLeave() }}>
|
||||
<RetinaImg name="icon-composer-send.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
<span className="send-action-text">{localized('Send now instead')}</span>
|
||||
</div>
|
||||
<div className="action" onClick={() => AppEnv.commands.dispatch('core:undo')}>
|
||||
<RetinaImg name="undo-icon@2x.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
<span className="undo-action-text">{localized('Undo')}</span>
|
||||
|
|
|
@ -46,10 +46,13 @@
|
|||
img {
|
||||
background-color: @background-primary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: fade(@black, 30%);
|
||||
border: 1px solid fade(@background-primary, 30%);
|
||||
}
|
||||
|
||||
.send-action-text,
|
||||
.undo-action-text {
|
||||
margin-left: 5px;
|
||||
color: @background-primary;
|
||||
|
|
|
@ -619,6 +619,7 @@
|
|||
"Send message": "Send message",
|
||||
"Send more than one message using the same %@ or subject line to compare open rates and reply rates.": "Send more than one message using the same %@ or subject line to compare open rates and reply rates.",
|
||||
"Send new messages from:": "Send new messages from:",
|
||||
"Send now instead": "Send now instead",
|
||||
"Send on your own schedule": "Send on your own schedule",
|
||||
"Sender Name": "Sender Name",
|
||||
"Sending": "Sending",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"About Mailspring": "A proposito di Mailspring",
|
||||
"Accept": "Accetta",
|
||||
"Account": "Account",
|
||||
"Account Color": "Colore account",
|
||||
"Account Details": "Dettagli account",
|
||||
"Account Label": "Etichetta del conto",
|
||||
"Account Settings": "Impostazioni account",
|
||||
|
@ -364,6 +365,7 @@
|
|||
"Make sure you have `libsecret` installed and a keyring is present. ": "Assicurati di aver installato `libsecret` e che sia presente un portachiavi.",
|
||||
"Manage": "Gestire",
|
||||
"Manage Accounts": "Gestisci account",
|
||||
"Manage Contacts": "Gestisci contatti",
|
||||
"Manage Billing": "Gestisci la fatturazione",
|
||||
"Manage Templates...": "Gestisci modelli...",
|
||||
"Manually": "Manualmente",
|
||||
|
@ -537,6 +539,7 @@
|
|||
"Reply Rate": "Tasso di risposta",
|
||||
"Reply to": "Rispondi a",
|
||||
"Reset": "Reimposta",
|
||||
"Reset Account Color": "Reimposta colore account",
|
||||
"Reset Accounts and Settings": "Reimposta account e impostazioni",
|
||||
"Reset Cache": "Svuota la cache",
|
||||
"Reset Configuration": "Ripristina configurazione",
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"\"Launch on system start\" only works in XDG-compliant desktop environments. To enable the Mailspring icon in the system tray, you may need to install libappindicator.": "\"시스템z 시작 시 자동실행\"은 XDG 호환 데스크톱 환경에서만 작동합니다. 시스템 트레이에서 Mailspring 아이콘을 사용하려면 libappindicator 패키지를 설치해야합니다.",
|
||||
"%1$@ of %2$@": "%1$@의 ❤ %1$@",
|
||||
"%@ cannot be attached because it is larger than 25MB.": "파일 %@의 크기가 25MB를 넘어가서 첨부할 수 없습니다.",
|
||||
"%@ has been installed and enabled. No need to restart! If you don't see the plugin loaded, check the console for errors.": "플러그인 %@이(가) 설치 및 활성화되었습니다. 다시 시작할 필요가 없습니다! 로드 된 플러그인이 표시되지 않으면 콘솔에서 오류를 확인하십시오.",
|
||||
"%@ has been installed and enabled. No need to restart! If you don't see the plugin loaded, check the console for errors.": "플러그인 %@이(가) 설치 및 활성화되었습니다. 다시 시작할 필요가 없습니다! 로드된 플러그인이 표시되지 않으면 콘솔에서 오류를 확인하십시오.",
|
||||
"%@ is a directory. Try compressing it and attaching it again.": "%@는 디렉토리입니다. 압축해서 첨부해보세요.",
|
||||
"%@ messages in this thread are hidden because they were moved to trash or spam.": "이 스레드의 %@개의 메세지는 휴지통이나 스팸으로 이동했기 때문에 숨겨져 있습니다.",
|
||||
"%@ others": "%@명",
|
||||
|
@ -36,12 +36,12 @@
|
|||
"Aliases": "별칭",
|
||||
"All": "전체보관함",
|
||||
"All Accounts": "모든 계정",
|
||||
"All Contact Previews Used": "사용 된 모든 연락처 미리보기",
|
||||
"All Contact Previews Used": "사용된 모든 연락처 미리보기",
|
||||
"All Mail": "전체 메일",
|
||||
"All Reminders Used": "사용 된 모든 알림",
|
||||
"All Scheduled Sends Used": "사용 된 모든 예약 전송",
|
||||
"All Reminders Used": "사용된 모든 알림",
|
||||
"All Scheduled Sends Used": "사용된 모든 예약 전송",
|
||||
"All Sharing Links Used": "모든 공유 링크 사용",
|
||||
"All Snoozes Used": "사용 된 모든 나중에 다시 알림",
|
||||
"All Snoozes Used": "사용된 모든 나중에 다시 알림",
|
||||
"All used up!": "다 써버렸네요!",
|
||||
"Allow insecure SSL": "비보안 SSL 허용",
|
||||
"Always show images from %@": "%@로부터 도착한 이미지 항상 보기",
|
||||
|
@ -78,7 +78,7 @@
|
|||
"Can't find the selected thread in your mailbox": "메일함에서 선택한 스레드를 찾을 수 없습니다.",
|
||||
"Cancel": "취소",
|
||||
"Cancel Send Later": "나중에 보내기 취소",
|
||||
"Cannot scan templates directory": "템플릿 디렉토리를 검색 할 수 없습니다.",
|
||||
"Cannot scan templates directory": "템플릿 디렉토리를 검색할 수 없습니다.",
|
||||
"Cannot send message": "메세지를 전송할 수 없음",
|
||||
"Cc": "참조:",
|
||||
"Certificate Error": "인증서 오류",
|
||||
|
@ -103,10 +103,10 @@
|
|||
"Cleanup Complete": "정리 완료",
|
||||
"Cleanup Error": "정리 오류",
|
||||
"Cleanup Started": "정리 시작",
|
||||
"Clear Selection": "명확한 선택",
|
||||
"Clear reminder": "지우기 알림",
|
||||
"Clear Selection": "선택 해제하기",
|
||||
"Clear reminder": "알림 지우기",
|
||||
"Click 'Learn More' to view instructions in our knowledge base.": "지식 베이스의 지침을 보려면 '자세히 알아보기'를 클릭하십시오.",
|
||||
"Click any theme to apply:": "적용 할 테마를 클릭하십시오 :",
|
||||
"Click any theme to apply:": "적용할 테마를 클릭하십시오 :",
|
||||
"Click shortcuts above to edit them. For even more control, you can edit the shortcuts file directly below.": "위의 바로 가기를 클릭하여 편집하십시오. 더 많은 바로가기를 편집하시려면, 바로 아래의 바로 가기 파일을 직접 편집하세요.",
|
||||
"Click to replace": "교체하려면 클릭하십시오.",
|
||||
"Click to upload": "클릭하여 업로드",
|
||||
|
@ -116,7 +116,7 @@
|
|||
"Collapse": "접기",
|
||||
"Collapse All": "모두 축소",
|
||||
"Combine your search queries with Gmail-style terms like %@ and %@ to find anything in your mailbox.": "%@나 %@와 같은 Gmail 스타일 검색어 쿼리를 이용해서 원하는 메일을 필터링해서 볼 수 있습니다.",
|
||||
"Comma-separated email addresses": "쉼표로 구분 된 이메일 주소",
|
||||
"Comma-separated email addresses": "쉼표로 구분된 이메일 주소",
|
||||
"Company / Domain Logo": "회사 / 도메인 로고",
|
||||
"Company overviews": "회사 개요",
|
||||
"Complete the IMAP and SMTP settings below to connect your account.": "아래의 IMAP 및 SMTP 설정을 완료하여 계정을 연결하십시오.",
|
||||
|
@ -141,9 +141,9 @@
|
|||
"Could not create plugin": "플러그인을 만들 수 없습니다.",
|
||||
"Could not find a file at path '%@'": "'%@'경로에서 파일을 찾을 수 없습니다.",
|
||||
"Could not install plugin": "플러그인을 설치할 수 없습니다.",
|
||||
"Could not reach %@. %@": "%@에 도달 할 수 없습니다. %@",
|
||||
"Could not reach %@. %@": "%@에 도달할 수 없습니다. %@",
|
||||
"Could not reach Mailspring. Please try again or contact support@getmailspring.com if the issue persists. (%@: %@)": "Mailspring에 연결할 수 없습니다. 문제가 지속되면 다시 시도하거나 support@getmailspring.com에 문의하십시오. (%@ : %@)",
|
||||
"Could not reset accounts and settings. Please delete the folder %@ manually.\n\n%@": "계정 및 설정을 초기화 할 수 없습니다. %@ 수동으로 폴더를 삭제하십시오 .\n\n %@",
|
||||
"Could not reset accounts and settings. Please delete the folder %@ manually.\n\n%@": "계정 및 설정을 초기화할 수 없습니다. %@ 수동으로 폴더를 삭제하십시오 .\n\n %@",
|
||||
"Create": "생성",
|
||||
"Create a Plugin": "플러그인 만들기",
|
||||
"Create a Theme": "테마 만들기",
|
||||
|
@ -325,7 +325,7 @@
|
|||
"Loading Messages": "메세지 로드 중",
|
||||
"Loading...": "로드 중...",
|
||||
"Local Data": "로컬 데이터",
|
||||
"Localized": "지역화 된",
|
||||
"Localized": "지역화된",
|
||||
"Log Data": "로그 데이터",
|
||||
"Look Up “%@”": "찾기: ‘%@’",
|
||||
"Looking for accounts...": "계정을 찾는 중 ...",
|
||||
|
@ -339,12 +339,12 @@
|
|||
"Mailspring Help": "Mailspring 도움말",
|
||||
"Mailspring Pro": "Mailspring Pro",
|
||||
"Mailspring Reminder": "Mailspring Reminder",
|
||||
"Mailspring can no longer authenticate with %@. The password or authentication may have changed.": "Mailspring은 %@로 더 이상 인증 할 수 없습니다. 암호 또는 인증이 변경되었을 수 있습니다.",
|
||||
"Mailspring can no longer authenticate with %@. The password or authentication may have changed.": "Mailspring은 %@로 더 이상 인증할 수 없습니다. 암호 또는 인증이 변경되었을 수 있습니다.",
|
||||
"Mailspring can't find your Drafts folder. To create and send mail, visit Preferences > Folders and choose a Drafts folder.": "Mailspring에서 임시 보관함 폴더를 찾을 수 없습니다. 메일을 작성하고 보내려면 환경 설정> 폴더로 이동하여 임시 보관함을 선택하십시오.",
|
||||
"Mailspring could not find the mailsync process. If you're building Mailspring from source, make sure mailsync.tar.gz has been downloaded and unpacked in your working copy.": "Mailspring은 mailsync 프로세스를 찾을 수 있습니다. Mailspring을 소스에서 빌드하는 경우, mailsync.tar.gz가 다운로드되었고, 작업 폴더에 압축 해제되었는지 것을 확인하십시오.",
|
||||
"Mailspring could not save an attachment because you have run out of disk space.": "디스크 공간이 부족하여 Mailspring이 첨부 파일을 저장할 수 없습니다.",
|
||||
"Mailspring could not save an attachment. Check that permissions are set correctly and try restarting Mailspring if the issue persists.": "Mailspring이 첨부 파일을 저장할 수 없습니다. 사용 권한이 올바르게 설정되어 있는지 확인하고 문제가 지속되면 Mailspring을 다시 시작하십시오.",
|
||||
"Mailspring could not spawn the mailsync process. %@": "Mailspring은 mailsync 프로세스를 생성 할 수 없습니다. %@",
|
||||
"Mailspring could not spawn the mailsync process. %@": "Mailspring은 mailsync 프로세스를 생성할 수 없습니다. %@",
|
||||
"Mailspring could not store your password securely. %@ For more information, visit %@": "Mailspring은 비밀번호를 안전하게 저장할 수 없습니다. %@ 자세한 내용은 %@",
|
||||
"Mailspring desktop notifications on Linux require Zenity. You may need to install it with your package manager.": "Linux에서 Mailspring의 바탕 화면 알림을 활성화하려면 Zenity가 필요합니다. 패키지 관리자와 함께 설치해야 할 수도 있습니다.",
|
||||
"Mailspring does not support stylesheets with the extension: %@": "Mailspring은 확장자가 있는 스타일 시트를 지원하지 않습니다 : %@",
|
||||
|
@ -354,7 +354,7 @@
|
|||
"One or more accounts are having connection issues.": "Mailspring은 오프라인 상태입니다.",
|
||||
"Mailspring is running in dev mode and may be slower!": "Mailspring이 현재 개발자 모드에서 실행 중입니다. 성능이 떨어질 수 있습니다.",
|
||||
"Mailspring is syncing this thread and it's attachments to the cloud. For long threads, this may take a moment.": "Mailspring은 이 스레드와 클라우드에 첨부 파일을 동기화합니다. 긴 스레드의 경우, 이 작업에는 약간의 시간이 걸릴 수 있습니다.",
|
||||
"Mailspring is unable to sync %@": "Mailspring이 %@을 (를) 동기화 할 수 없습니다.",
|
||||
"Mailspring is unable to sync %@": "Mailspring이 %@을 (를) 동기화할 수 없습니다.",
|
||||
"Mailspring reset the local cache for %@ in %@ seconds. Your mailbox will now begin to sync again.": "Mailspring은 %@ 초 동안 %@ 초 동안 로컬 캐시를 초기화합니다. 이제 편지함이 다시 동기화되기 시작합니다.",
|
||||
"Mailspring shows you everything about your contacts right inside your inbox. See LinkedIn profiles, Twitter bios, message history, and more.": "Mailspring을 이용해 받은 편지함에서 연락처 정보를 바로 확인할 수 있습니다! LinkedIn, Twitter와 지난 메세지 기록을 확인하세요.",
|
||||
"Mailspring was unable to modify your keymaps at %@.": "Mailspring은 %@에서 키맵을 수정할 수 없었습니다.",
|
||||
|
@ -605,7 +605,7 @@
|
|||
"Sending": "보내는 중",
|
||||
"Sending in %@": "%@로 보내기",
|
||||
"Sending in a few seconds": "몇 초 후에 보냄",
|
||||
"Sending is not enabled for this account.": "이 계정에서는 메일 발신을 할 수 없습니다.",
|
||||
"Sending is not enabled for this account.": "이 계정에서는 메일을 발신할 수 없습니다.",
|
||||
"Sending message": "보내는 메세지",
|
||||
"Sending now": "보내는 중",
|
||||
"Sending soon...": "곧 보냄 ...",
|
||||
|
@ -658,13 +658,13 @@
|
|||
"Sorry, the file you selected does not look like an image. Please choose a file with one of the following extensions: %@": "죄송합니다. 선택한 파일이 이미지처럼 보이지 않습니다. 다음 확장명 중 하나의 파일을 선택하십시오. %@",
|
||||
"Sorry, this account does not appear to have an inbox folder so this feature is disabled.": "죄송합니다. 이 계정에는 받은 편지함 폴더가 없으므로 이 기능을 사용할 수 없습니다.",
|
||||
"Sorry, this folder does not exist.": "죄송합니다.이 폴더는 존재하지 않습니다.",
|
||||
"Sorry, we can't interpret %@ as a valid date.": "죄송합니다. %@을 유효한 날짜로 해석 할 수 없습니다.",
|
||||
"Sorry, we can't parse %@ as a valid date.": "죄송합니다. 유효한 날짜로 %@을 (를) 파싱 할 수 없습니다.",
|
||||
"Sorry, we can't interpret %@ as a valid date.": "죄송합니다. %@을 유효한 날짜로 해석할 수 없습니다.",
|
||||
"Sorry, we can't parse %@ as a valid date.": "죄송합니다. 유효한 날짜로 %@을 (를) 파싱할 수 없습니다.",
|
||||
"Sorry, we couldn't save your signature image to Mailspring's servers. Please try again.\n\n(%@)": "죄송합니다. 서명 이미지를 Mailspring의 서버에 저장할 수 없습니다. 다시 시도하십시오. \n \n (%@)",
|
||||
"Sorry, we had trouble logging you in": "죄송합니다. 로그인하는 데 문제가 있었습니다.",
|
||||
"Sorry, we were unable to complete the translation request.": "죄송합니다. 번역 요청을 완료하지 못했습니다.",
|
||||
"Sorry, we were unable to contact the Mailspring servers to share this thread.\n\n%@": "죄송합니다. 이 스레드를 공유하려고 시도했으나 현재 Mailspring 서버에 접속할 수 없습니다. \n \n %@",
|
||||
"Sorry, you can't attach more than 25MB of attachments": "죄송합니다. 첨부 파일을 25MB 이상 첨부 할 수 없습니다.",
|
||||
"Sorry, you can't attach more than 25MB of attachments": "죄송합니다. 첨부 파일을 25MB 이상 첨부할 수 없습니다.",
|
||||
"Sorry, you must create plugins in the dev/packages folder.": "죄송합니다. dev / packages 폴더에 플러그인을 만들어야합니다.",
|
||||
"Sorry, you must give your plugin a unique name.": "죄송합니다. 플러그인에 고유한 이름을 지정해야합니다.",
|
||||
"Sorry, your SMTP server does not support basic username / password authentication.": "죄송합니다. SMTP 서버는 기본 사용자 이름 / 비밀번호 인증을 지원하지 않습니다.",
|
||||
|
@ -689,7 +689,7 @@
|
|||
"Switching back to a signature template will overwrite the custom HTML you've entered.": "서명 템플릿으로 다시 전환하면 입력한 사용자 정의 HTML을 덮어 씁니다.",
|
||||
"Symbols": "기호",
|
||||
"Sync New Mail Now": "새 메일 동기화",
|
||||
"Sync this conversation to the cloud and anyone with the secret link can read it and download attachments.": "이 대화를 클라우드와 동기화하면 비밀 링크가 있는 모든 사람이 이 대화를 읽고 첨부 파일을 다운로드 할 수 있습니다.",
|
||||
"Sync this conversation to the cloud and anyone with the secret link can read it and download attachments.": "이 대화를 클라우드와 동기화하면 비밀 링크가 있는 모든 사람이 이 대화를 읽고 첨부 파일을 다운로드할 수 있습니다.",
|
||||
"Syncing": "동기화 중",
|
||||
"Syncing your mailbox": "메일함 동기화 중",
|
||||
"TLS Not Available": "TLS를 사용할 수 없음",
|
||||
|
@ -742,7 +742,7 @@
|
|||
"Title": "제목",
|
||||
"To": "받는 사람",
|
||||
"To create a template you need to fill the body of the current draft.": "템플릿을 만들려면 현재 초안의 본문을 채워야합니다.",
|
||||
"To develop plugins, you should run Mailspring with debug flags. This gives you better error messages, the debug version of React, and more. You can disable it at any time from the Developer menu.": "플러그인을 개발하려면 디버그 플래그로 Mailspring을 실행해야합니다. 이렇게하면 더 나은 오류 메세지와 React의 디버그 버전 등을 얻을 수 있습니다. 개발자 메뉴에서 언제든지 비활성화 할 수 있습니다.",
|
||||
"To develop plugins, you should run Mailspring with debug flags. This gives you better error messages, the debug version of React, and more. You can disable it at any time from the Developer menu.": "플러그인을 개발하려면 디버그 플래그로 Mailspring을 실행해야합니다. 이렇게하면 더 나은 오류 메세지와 React의 디버그 버전 등을 얻을 수 있습니다. 개발자 메뉴에서 언제든지 비활성화할 수 있습니다.",
|
||||
"To listen for the Gmail Oauth response, Mailspring needs to start a webserver on port ${LOCAL_SERVER_PORT}. Please go back and try linking your account again. If this error persists, use the IMAP/SMTP option with a Gmail App Password.\n\n%@": "Gmail Oauth 응답을 수신 대기하려면 Mailspring이 $ {LOCAL_SERVER_PORT} 포트에서 웹 서버를 시작해야합니다. 돌아가서 계정을 다시 연결하십시오. 이 오류가 계속 발생하면 Gmail 앱 비밀번호와 함께 IMAP / SMTP 옵션을 사용하십시오. \n \n %@",
|
||||
"Today": "오늘",
|
||||
"Toggle Bold": "볼드체 토글",
|
||||
|
@ -774,7 +774,7 @@
|
|||
"Uhoh - that's a pro feature!": "Uhoh - 프로 기능입니다!",
|
||||
"Unable to Add Account": "계정을 추가 할 수 없습니다.",
|
||||
"Unable to Start Local Server": "로컬 서버를 시작할 수 없습니다.",
|
||||
"Unable to download %@. Check your network connection and try again. %@": "%@을 (를) 다운로드 할 수 없습니다. 네트워크 연결을 확인하고 다시 시도하십시오. %@",
|
||||
"Unable to download %@. Check your network connection and try again. %@": "%@을 (를) 다운로드할 수 없습니다. 네트워크 연결을 확인하고 다시 시도하십시오. %@",
|
||||
"Unable to read package.json for %@: %@": "%@에 대한 package.json을 읽을 수 없습니다 : %@",
|
||||
"Unarchived %@": "보관되지 않은 %@",
|
||||
"Underline": "밑줄체",
|
||||
|
@ -819,7 +819,7 @@
|
|||
"Visit Windows Settings to finish making Mailspring your mail client": "Windows 설정을 방문하여 Mailspring을 메일 클라이언트로 만드십시오.",
|
||||
"We encountered a problem moving to the Applications folder. Try quitting the application and moving it manually.": "응용 프로그램 폴더로 이동하는 중 문제가 발생했습니다. 응용 프로그램을 종료하고 수동으로 이동해보십시오.",
|
||||
"We encountered a problem with your local email database. %@\n\nCheck that no other copies of Mailspring are running and click Rebuild to reset your local cache.": "로컬 이메일 데이터베이스에 문제가 발생했습니다. %@ \n \n 다른 Mailspring 사본이 실행되고 있지 않은지 확인하고 재구성을 클릭하여 로컬 캐시를 초기화하십시오.",
|
||||
"We encountered a problem with your local email database. We will now attempt to rebuild it.": "로컬 이메일 데이터베이스에 문제가 발생했습니다. 우리는 이제 그것을 재구성하려고 시도 할 것입니다.",
|
||||
"We encountered a problem with your local email database. We will now attempt to rebuild it.": "로컬 이메일 데이터베이스에 문제가 발생했습니다. 우리는 이제 그것을 재구성하려고 시도할 것입니다.",
|
||||
"We encountered an SMTP Gateway error that prevented this message from being delivered to all recipients. The message was only sent successfully to these recipients:\n%@\n\nError: %@": "이 메세지가 모든받는 사람에게 배달되지 못하게하는 SMTP 게이트웨이 오류가 발생했습니다. 메세지는 다음 받는 사람에게만 성공적으로 전송되었습니다. \n \n \n \n 오류 : %@",
|
||||
"We were unable to deliver this message to some recipients. Click 'See Details' for more information.": "이 메세지를 일부 수신자에게 전달할 수 없습니다. 자세한 내용은 '세부 정보보기'를 클릭하십시오.",
|
||||
"We were unable to deliver this message.": "이 메세지를 전달할 수 없었습니다.",
|
||||
|
@ -841,13 +841,13 @@
|
|||
"Yes": "네!",
|
||||
"You": "나",
|
||||
"You are using %@, which is free! You can try pro features like snooze, send later, read receipts and reminders a few times a week.": "무료인 %@을 사용하고 있습니다! 일주일에 몇 번 다시 알림, 나중에 보내기, 영수증 및 알림 읽기와 같은 프로 기능을 사용해 볼 수 있습니다.",
|
||||
"You can add reminders to %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 각 %2$@ 전자 메일에 %1$@ 전자 메일에 리마인더을 추가 할 수 있습니다.",
|
||||
"You can add reminders to %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 각 %2$@ 전자 메일에 %1$@ 전자 메일에 리마인더을 추가할 수 있습니다.",
|
||||
"You can choose a shortcut set to use keyboard shortcuts of familiar email clients. To edit a shortcut, click it in the list below and enter a replacement on the keyboard.": "익숙한 이메일 클라이언트의 키보드 단축키를 사용하도록 단축키를 선택할 수 있습니다. 단축키를 편집하려면 아래 목록에서 해당 단축키를 클릭하고 키보드에서 대체 문자를 입력하십시오.",
|
||||
"You can get open and click notifications for %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 %1$@ 이메일을 %2$@ 각각 열어보고 알림을 받을 수 있습니다.",
|
||||
"You can schedule sending of %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 %1$@ 개의 이메일을 %2$@ (으)로 전송하도록 예약 할 수 있습니다.",
|
||||
"You can share %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic과 함께 %1$@ 개의 이메일을 %2$@와 공유 할 수 있습니다.",
|
||||
"You can snooze %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 각 %2$@ (으)로 이메일을 나중에 다시 알림 할 수 있습니다.",
|
||||
"You can switch back to stable from the preferences.": "환경 설정에서 다시 안정화 버전으로 전환 할 수 있습니다.",
|
||||
"You can schedule sending of %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 %1$@ 개의 이메일을 %2$@ (으)로 전송하도록 예약할 수 있습니다.",
|
||||
"You can share %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic과 함께 %1$@ 개의 이메일을 %2$@와 공유할 수 있습니다.",
|
||||
"You can snooze %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 각 %2$@ (으)로 이메일을 나중에 다시 알릴 수 있습니다.",
|
||||
"You can switch back to stable from the preferences.": "환경 설정에서 다시 안정화 버전으로 전환할 수 있습니다.",
|
||||
"You can view contact profiles for %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 각 %2$@ 전자 메일의 연락처 프로파일을 볼 수 있습니다.",
|
||||
"You haven't created any mail rules. To get started, define a new rule above and tell Mailspring how to process your inbox.": "메일 규칙을 만들지 않았습니다. 시작하려면 위의 새 규칙을 정의하고 Mailspring에 받은 편지함을 처리하는 방법을 알려줍니다.",
|
||||
"You may need to %@ to your Yandex account before connecting email apps. If you use two-factor auth, you need to create an %@ for Mailspring.": "이메일 앱을 연결하기 전에 Yandex 계정에 %@해야 할 수도 있습니다. 2 단계 인증을 사용하는 경우 Mailspring 용 %@을 만들어야합니다.",
|
||||
|
@ -862,10 +862,10 @@
|
|||
"Please consider paying for Mailspring Pro!": "Mailspring Pro를 사용해보세요!",
|
||||
"You've reached your quota": "할당량에 도달했습니다.",
|
||||
"Your Mailspring ID is missing required fields - you may need to reset Mailspring. %@": "Mailspring ID에 필수 입력란이 누락되었습니다. Mailspring을 초기화해야 할 수도 있습니다. %@",
|
||||
"Your `Sent Mail` folder could not be automatically detected. Visit Preferences > Folders to choose a Sent folder and then try again.": "'보낸 편지함'폴더를 자동으로 검색 할 수 없습니다. 환경 설정> 폴더로 이동하여 보낸 편지함 폴더를 선택한 다음 다시 시도하십시오.",
|
||||
"Your `Trash` folder could not be automatically detected. Visit Preferences > Folders to choose a Trash folder and then try again.": "'휴지통'폴더를 자동으로 검색 할 수 없습니다. 환경 설정> 폴더로 이동하여 휴지통 폴더를 선택한 다음 다시 시도하십시오.",
|
||||
"Your `Sent Mail` folder could not be automatically detected. Visit Preferences > Folders to choose a Sent folder and then try again.": "'보낸 편지함'폴더를 자동으로 검색할 수 없습니다. 환경 설정> 폴더로 이동하여 보낸 편지함 폴더를 선택한 다음 다시 시도하십시오.",
|
||||
"Your `Trash` folder could not be automatically detected. Visit Preferences > Folders to choose a Trash folder and then try again.": "'휴지통'폴더를 자동으로 검색할 수 없습니다. 환경 설정> 폴더로 이동하여 휴지통 폴더를 선택한 다음 다시 시도하십시오.",
|
||||
"Your name": "내 이름",
|
||||
"Your updated localization will be reviewed and included in a future version of Mailspring.": "업데이트 된 현지화가 검토되어 향후 버전의 Mailspring에 포함됩니다.",
|
||||
"Your updated localization will be reviewed and included in a future version of Mailspring.": "업데이트된 현지화가 검토되어 향후 버전의 Mailspring에 포함됩니다.",
|
||||
"Zoom": "최대화/복원",
|
||||
"an email address": "이메일 주소",
|
||||
"an email subject": "이메일 제목",
|
||||
|
@ -898,7 +898,7 @@
|
|||
"these instructions": "이 지침들",
|
||||
"threads": "메세지",
|
||||
"week": "주",
|
||||
"All Translations Used": "사용 된 모든 번역",
|
||||
"All Translations Used": "사용된 모든 번역",
|
||||
"Always translate %@": "항상 %@ 번역",
|
||||
"Automatic Translation": "자동 번역",
|
||||
"Copy mailbox permalink": "사서함 permalink 복사",
|
||||
|
@ -917,5 +917,5 @@
|
|||
"Translating from %1$@ to %2$@.": "%1$@에서 %2$@로 번역 중입니다.",
|
||||
"Unfortunately, translation services bill per character and we can't offer this feature for free.": "유감스럽게도 번역 서비스는 문자 당 요금을 청구하므로 이 기능은 무료로 제공되지 않습니다.",
|
||||
"View activity": "활동보기",
|
||||
"You can translate up to %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 각 %2$@까지 %1$@ 개의 전자 메일을 번역 할 수 있습니다."
|
||||
"You can translate up to %1$@ emails each %2$@ with Mailspring Basic.": "Mailspring Basic을 사용하여 각 %2$@까지 %1$@ 개의 전자 메일을 번역할 수 있습니다."
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
64
app/package-lock.json
generated
64
app/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "mailspring",
|
||||
"version": "1.10.4",
|
||||
"version": "1.10.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mailspring",
|
||||
"version": "1.10.4",
|
||||
"version": "1.10.7",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bengotow/slate-edit-list": "github:bengotow/slate-edit-list#b868e108",
|
||||
|
@ -460,9 +460,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/array.prototype.flat/node_modules/object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
@ -1667,9 +1667,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/enzyme/node_modules/object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
@ -4821,9 +4821,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/string.prototype.trim/node_modules/object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
@ -4972,9 +4972,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/string.prototype.trimend/node_modules/object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
@ -5123,9 +5123,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/string.prototype.trimstart/node_modules/object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
@ -6001,9 +6001,9 @@
|
|||
}
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
@ -6918,9 +6918,9 @@
|
|||
}
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
@ -9554,9 +9554,9 @@
|
|||
}
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
@ -9656,9 +9656,9 @@
|
|||
}
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
@ -9758,9 +9758,9 @@
|
|||
}
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz",
|
||||
"integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
|
||||
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "mailspring",
|
||||
"productName": "Mailspring",
|
||||
"version": "1.10.5",
|
||||
"version": "1.10.8",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/foundry376/mailspring.git"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import classnames from 'classnames';
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, CSSProperties } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Actions from '../flux/actions';
|
||||
|
@ -67,7 +67,9 @@ function buildContextMenu(fns: {
|
|||
label: localized('Save Into...'),
|
||||
});
|
||||
}
|
||||
require('@electron/remote').Menu.buildFromTemplate(template).popup({});
|
||||
require('@electron/remote')
|
||||
.Menu.buildFromTemplate(template)
|
||||
.popup({});
|
||||
}
|
||||
|
||||
const ProgressBar: React.FunctionComponent<{
|
||||
|
@ -284,11 +286,17 @@ export class AttachmentItem extends Component<AttachmentItemProps> {
|
|||
}
|
||||
}
|
||||
|
||||
export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgProps?: any }> {
|
||||
interface ImageAttachmentItemProps extends AttachmentItemProps {
|
||||
onResized: (width: number, height: number) => void;
|
||||
imgProps?: { width: number; height: number };
|
||||
}
|
||||
|
||||
export class ImageAttachmentItem extends Component<ImageAttachmentItemProps> {
|
||||
static displayName = 'ImageAttachmentItem';
|
||||
|
||||
static propTypes = {
|
||||
imgProps: PropTypes.object,
|
||||
onResized: PropTypes.func,
|
||||
...propTypes,
|
||||
};
|
||||
|
||||
|
@ -308,7 +316,7 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
|
|||
};
|
||||
|
||||
renderImage() {
|
||||
const { download, filePath, draggable } = this.props;
|
||||
const { download, filePath, draggable, imgProps } = this.props;
|
||||
if (download && download.percent <= 5) {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100px' }}>
|
||||
|
@ -316,9 +324,22 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const src =
|
||||
download && download.percent < 100 ? `${filePath}?percent=${download.percent}` : filePath;
|
||||
return <img draggable={draggable} src={src} alt="" onLoad={this._onImgLoaded} />;
|
||||
download && download.percent < 100 ? `${filePath}?percent=${download.percent}` : filePath,
|
||||
styles: CSSProperties = {};
|
||||
|
||||
if (imgProps) {
|
||||
if (imgProps.height) {
|
||||
styles.height = `${imgProps.height}px`;
|
||||
}
|
||||
|
||||
if (imgProps.width) {
|
||||
styles.width = `${imgProps.width}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return <img draggable={draggable} src={src} alt="" onLoad={this._onImgLoaded} style={styles} />;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -340,6 +361,7 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
|
|||
onSaveAttachment,
|
||||
...extraProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`nylas-attachment-item image-attachment-item ${className || ''}`}
|
||||
|
@ -366,7 +388,132 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
|
|||
{this.renderImage()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="resizer" onMouseDown={this._resizeStart}>
|
||||
<i className="gg-arrows-expand-left"></i>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _pData = { x: 0, y: 0, eH: 0 };
|
||||
private _shiftData = {
|
||||
held: false,
|
||||
ratio: { wh: 0, hw: 0 },
|
||||
};
|
||||
private _editor = () => document.querySelector('.compose-body') as HTMLDivElement;
|
||||
|
||||
private _resizeImage = (
|
||||
ev: (
|
||||
| MouseEvent
|
||||
| {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
) & { useWH?: boolean }
|
||||
) => {
|
||||
const img = document.querySelector(
|
||||
'.image-attachment-item[data-resizing] .file-preview img'
|
||||
) as HTMLImageElement,
|
||||
editor = this._editor();
|
||||
|
||||
if (img) {
|
||||
let newWidth = ev.x - img.x,
|
||||
newHeight = ev.y - img.y;
|
||||
const width = ev.useWH ? newHeight * this._shiftData.ratio.wh : img.width;
|
||||
|
||||
if (!this._shiftData.held) {
|
||||
if (
|
||||
(newWidth - width) * this._shiftData.ratio.hw >
|
||||
(newHeight - img.height) * this._shiftData.ratio.wh
|
||||
) {
|
||||
newHeight = newWidth * this._shiftData.ratio.hw;
|
||||
} else {
|
||||
newWidth = newHeight * this._shiftData.ratio.wh;
|
||||
}
|
||||
}
|
||||
|
||||
img.style.width = `${newWidth}px`;
|
||||
img.style.height = `${newHeight}px`;
|
||||
}
|
||||
|
||||
const firstChild = editor.children[0] as HTMLDivElement;
|
||||
if (Number.parseInt(editor.style.flexBasis) < firstChild.offsetHeight) {
|
||||
editor.style.flexBasis = `${firstChild.offsetHeight}px`;
|
||||
}
|
||||
|
||||
this._pData = { x: ev.x, y: ev.y, eH: editor.clientHeight };
|
||||
};
|
||||
|
||||
private _resizeImageKeyPress = (ev: KeyboardEvent) => {
|
||||
const oldHeld = this._shiftData.held;
|
||||
|
||||
this._shiftData.held = ev.shiftKey;
|
||||
|
||||
if (oldHeld !== ev.shiftKey) {
|
||||
this._resizeImage({
|
||||
x: this._pData.x,
|
||||
y: this._pData.y,
|
||||
useWH: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _resizeStart = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const parent = ev.currentTarget.parentNode as HTMLDivElement,
|
||||
imgEl = parent.querySelector('.file-preview img') as HTMLImageElement,
|
||||
editor = this._editor();
|
||||
|
||||
this._pData = { x: ev.pageX, y: ev.pageY, eH: editor.clientHeight };
|
||||
this._shiftData.held = ev.shiftKey;
|
||||
this._shiftData.ratio = { wh: imgEl.width / imgEl.height, hw: imgEl.height / imgEl.width };
|
||||
|
||||
parent.dataset.resizing = '1';
|
||||
imgEl.draggable = false;
|
||||
|
||||
editor.addEventListener('mousemove', this._resizeImage);
|
||||
editor.addEventListener('mouseup', this._resizeEnd);
|
||||
editor.parentElement.parentElement.parentElement.addEventListener(
|
||||
'mouseleave',
|
||||
this._resizeEnd
|
||||
);
|
||||
editor.addEventListener('keydown', this._resizeImageKeyPress);
|
||||
editor.addEventListener('keyup', this._resizeImageKeyPress);
|
||||
|
||||
editor.style.flexBasis = `${(editor.children[0] as HTMLDivElement).offsetHeight}px`;
|
||||
};
|
||||
|
||||
private _resizeEnd = (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const editor = this._editor(),
|
||||
target = editor.querySelector('.image-attachment-item[data-resizing]') as HTMLDivElement;
|
||||
|
||||
if (editor.clientHeight == this._pData.eH && target) {
|
||||
delete target.dataset.resizing;
|
||||
|
||||
(target.querySelector('.file-preview img') as HTMLImageElement).draggable = true;
|
||||
editor.removeEventListener('mousemove', this._resizeImage);
|
||||
editor.removeEventListener('mouseup', this._resizeEnd);
|
||||
editor.parentElement.parentElement.parentElement.removeEventListener(
|
||||
'mouseleave',
|
||||
this._resizeEnd
|
||||
);
|
||||
editor.removeEventListener('keydown', this._resizeImageKeyPress);
|
||||
editor.removeEventListener('keyup', this._resizeImageKeyPress);
|
||||
|
||||
editor.animate([{ flexBasis: `${(editor.children[0] as HTMLDivElement).offsetHeight}px` }], {
|
||||
duration: 500,
|
||||
iterations: 1,
|
||||
}).onfinish = () => {
|
||||
editor.style.flexBasis = '';
|
||||
};
|
||||
|
||||
const img = target.querySelector('.file-preview img') as HTMLImageElement;
|
||||
this.props.onResized(img.width, img.height);
|
||||
} else {
|
||||
this._pData.eH = editor.clientHeight;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,17 +3,18 @@ import { ImageAttachmentItem } from 'mailspring-component-kit';
|
|||
import { AttachmentStore } from 'mailspring-exports';
|
||||
import { isQuoteNode } from './base-block-plugins';
|
||||
import { ComposerEditorPlugin } from './types';
|
||||
import { Editor, Node } from 'slate';
|
||||
import { Editor, Inline, Node } from 'slate';
|
||||
import { schema } from './conversion';
|
||||
|
||||
export const IMAGE_TYPE = 'image';
|
||||
|
||||
function ImageNode(props) {
|
||||
const { attributes, node, editor, targetIsHTML, isFocused } = props;
|
||||
const contentId = node.data.get ? node.data.get('contentId') : node.data.contentId;
|
||||
const contentId = node.data.get ? node.data.get('contentId') : node.data.contentId,
|
||||
imgProps = node.data.get ? node.data.get('imgProps') : node.data.imgProps;
|
||||
|
||||
if (targetIsHTML) {
|
||||
return <img alt="" src={`cid:${contentId}`} />;
|
||||
return <img alt="" src={`cid:${contentId}`} width={imgProps?.width} height={imgProps?.height} />;
|
||||
}
|
||||
|
||||
const { draft } = editor.props.propsForPlugins;
|
||||
|
@ -29,6 +30,22 @@ function ImageNode(props) {
|
|||
filePath={AttachmentStore.pathForFile(file)}
|
||||
displayName={file.filename}
|
||||
onRemoveAttachment={() => editor.removeNodeByKey(node.key)}
|
||||
imgProps={imgProps}
|
||||
onResized={(width, height) => {
|
||||
const e = editor as Editor,
|
||||
n = node as Inline,
|
||||
newN = {
|
||||
key: n.key,
|
||||
object: n.object,
|
||||
data: n.data.asMutable(),
|
||||
type: n.type,
|
||||
nodes: n.nodes.asMutable(),
|
||||
};
|
||||
|
||||
newN.data = newN.data.set('imgProps', { width: width, height: height });
|
||||
|
||||
e.setNodeByKey(n.key, newN);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -42,20 +59,25 @@ function renderNode(props, editor: Editor = null, next = () => {}) {
|
|||
|
||||
const rules = [
|
||||
{
|
||||
deserialize(el, next) {
|
||||
if (el.tagName.toLowerCase() === 'img' && (el.getAttribute('src') || '').startsWith('cid:')) {
|
||||
return {
|
||||
object: 'inline',
|
||||
nodes: [],
|
||||
type: IMAGE_TYPE,
|
||||
data: {
|
||||
contentId: el
|
||||
.getAttribute('src')
|
||||
.split('cid:')
|
||||
.pop(),
|
||||
},
|
||||
};
|
||||
}
|
||||
deserialize(el: HTMLElement, next) {
|
||||
if (el.tagName.toLowerCase() === 'img')
|
||||
if ((el.getAttribute('src') || '').startsWith('cid:')) {
|
||||
return {
|
||||
object: 'inline',
|
||||
nodes: [],
|
||||
type: IMAGE_TYPE,
|
||||
data: {
|
||||
contentId: el
|
||||
.getAttribute('src')
|
||||
.split('cid:')
|
||||
.pop(),
|
||||
imgProps: {
|
||||
width: Number.parseInt(el.getAttribute('width')),
|
||||
height: Number.parseInt(el.getAttribute('height')),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
serialize(obj, children) {
|
||||
if (obj.object !== 'inline') return;
|
||||
|
|
|
@ -45,7 +45,7 @@ Any `props` added to the <EventedIFrame> are passed to the iFrame it renders.
|
|||
Section: Component Kit
|
||||
*/
|
||||
export class EventedIFrame extends React.Component<
|
||||
EventedIFrameProps & React.HTMLProps<HTMLDivElement>
|
||||
EventedIFrameProps & React.HTMLProps<HTMLIFrameElement>
|
||||
> {
|
||||
static displayName = 'EventedIFrame';
|
||||
|
||||
|
@ -354,14 +354,14 @@ export class EventedIFrame extends React.Component<
|
|||
new MenuItem({
|
||||
label: localized('Save Image') + '...',
|
||||
click() {
|
||||
AppEnv.showSaveDialog({ defaultPath: srcFilename }, function (path) {
|
||||
AppEnv.showSaveDialog({ defaultPath: srcFilename }, function(path) {
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
const oReq = new XMLHttpRequest();
|
||||
oReq.open('GET', src, true);
|
||||
oReq.responseType = 'arraybuffer';
|
||||
oReq.onload = function () {
|
||||
oReq.onload = function() {
|
||||
const buffer = Buffer.from(new Uint8Array(oReq.response));
|
||||
fs.writeFile(path, buffer, err => shell.showItemInFolder(path));
|
||||
};
|
||||
|
@ -377,7 +377,7 @@ export class EventedIFrame extends React.Component<
|
|||
const img = new Image();
|
||||
img.addEventListener(
|
||||
'load',
|
||||
function () {
|
||||
function() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
|
|
|
@ -45,7 +45,9 @@ export default class OpenIdentityPageButton extends React.Component<
|
|||
content: this.props.label,
|
||||
}).then(url => {
|
||||
this.setState({ loading: false });
|
||||
shell.openExternal(url);
|
||||
if (/^https?:\/\/.+/i.test(url)) {
|
||||
shell.openExternal(url);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -151,14 +151,13 @@ export default class Webview extends React.Component<WebviewProps, WebviewState>
|
|||
};
|
||||
|
||||
_onNewWindow = e => {
|
||||
const { protocol } = url.parse(e.url);
|
||||
if (protocol === 'http:' || protocol === 'https:') {
|
||||
if (/^https?:\/\/.+/i.test(e.url)) {
|
||||
shell.openExternal(e.url);
|
||||
}
|
||||
};
|
||||
|
||||
_onConsoleMessage = e => {
|
||||
if (/^http.+/i.test(e.message)) {
|
||||
if (/^https?:\/\/.+/i.test(e.message)) {
|
||||
shell.openExternal(e.message);
|
||||
}
|
||||
console.log('Guest page logged a message:', e.message);
|
||||
|
|
|
@ -389,27 +389,40 @@ const DateUtils = {
|
|||
*
|
||||
* The returned date/time format depends on how long ago the timestamp is.
|
||||
*/
|
||||
shortTimeString(datetime) {
|
||||
shortTimeString(datetime: Date) {
|
||||
const now = moment();
|
||||
const diff = now.diff(datetime, 'days', true);
|
||||
const isSameDay = now.isSame(datetime, 'days');
|
||||
let format = null;
|
||||
const opts: Intl.DateTimeFormatOptions = {
|
||||
hourCycle: AppEnv.config.get('core.workspace.use24HourClock') ? 'h23' : 'h12',
|
||||
};
|
||||
|
||||
if (diff <= 1 && isSameDay) {
|
||||
// Time if less than 1 day old
|
||||
format = DateUtils.getTimeFormat(null);
|
||||
} else if (diff < 2 && !isSameDay) {
|
||||
// Month and day with time if up to 2 days ago
|
||||
format = `MMM D, ${DateUtils.getTimeFormat(null)}`;
|
||||
} else if (diff >= 2 && diff < 365) {
|
||||
// Month and day up to 1 year old
|
||||
format = 'MMM D';
|
||||
opts.hour = 'numeric';
|
||||
opts.minute = '2-digit';
|
||||
} else if (diff < 5 && !isSameDay) {
|
||||
// Weekday with time if up to 2 days ago
|
||||
//opts.month = 'short';
|
||||
//opts.day = 'numeric';
|
||||
opts.weekday = 'short';
|
||||
opts.hour = 'numeric';
|
||||
opts.minute = '2-digit';
|
||||
} else {
|
||||
// Month, day and year if over a year old
|
||||
format = 'MMM D YYYY';
|
||||
if (diff < 365) {
|
||||
// Month and day up to 1 year old
|
||||
opts.month = 'short';
|
||||
opts.day = 'numeric';
|
||||
} else {
|
||||
// Month, day and year if over a year old
|
||||
opts.year = 'numeric';
|
||||
opts.month = 'short';
|
||||
opts.day = 'numeric';
|
||||
}
|
||||
return datetime.toLocaleDateString(navigator.language, opts);
|
||||
}
|
||||
|
||||
return moment(datetime).format(format);
|
||||
return datetime.toLocaleTimeString(navigator.language, opts);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -418,11 +431,14 @@ const DateUtils = {
|
|||
* @param {Date} datetime - Timestamp
|
||||
* @return {String} Formated date/time
|
||||
*/
|
||||
mediumTimeString(datetime) {
|
||||
let format = 'MMMM D, YYYY, ';
|
||||
format += DateUtils.getTimeFormat({ seconds: false, upperCase: true, timeZone: false });
|
||||
|
||||
return moment(datetime).format(format);
|
||||
mediumTimeString(datetime: Date) {
|
||||
return datetime.toLocaleTimeString(navigator.language, {
|
||||
hourCycle: AppEnv.config.get('core.workspace.use24HourClock') ? 'h23' : 'h12',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
second: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -431,13 +447,20 @@ const DateUtils = {
|
|||
* @param {Date} datetime - Timestamp
|
||||
* @return {String} Formated date/time
|
||||
*/
|
||||
fullTimeString(datetime) {
|
||||
let format = 'dddd, MMMM Do YYYY, ';
|
||||
format += DateUtils.getTimeFormat({ seconds: true, upperCase: true, timeZone: true });
|
||||
fullTimeString(datetime: Date) {
|
||||
// ISSUE: this does drop ordinal. There is this:
|
||||
// -> new Intl.PluralRules(LOCALE, { type: "ordinal" }).select(dateTime.getDay())
|
||||
// which may work with the below regex, though localisation is required
|
||||
// `(?<!\d)${dateTime.getDay()}(?!\d)` replace `$1${localise(ordinal)}`
|
||||
|
||||
return moment(datetime)
|
||||
.tz(tz)
|
||||
.format(format);
|
||||
return datetime.toLocaleTimeString(navigator.language, {
|
||||
hourCycle: AppEnv.config.get('core.workspace.use24HourClock') ? 'h23' : 'h12',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
second: undefined,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ export class DefaultClientHelperWindows implements DCH {
|
|||
}
|
||||
|
||||
async resetURLScheme() {
|
||||
const { response } = awaitrequire('@electron/remote').dialog.showMessageBox({
|
||||
const { response } = await require('@electron/remote').dialog.showMessageBox({
|
||||
type: 'info',
|
||||
buttons: [localized('Learn More')],
|
||||
message: localized('Visit Windows Settings to change your default mail client'),
|
||||
|
@ -58,7 +58,7 @@ export class DefaultClientHelperWindows implements DCH {
|
|||
}
|
||||
}
|
||||
|
||||
registerForURLScheme(scheme: string, callback = (error?: Error) => { }) {
|
||||
registerForURLScheme(scheme: string, callback = (error?: Error) => {}) {
|
||||
// Ensure that our registry entires are present
|
||||
const WindowsUpdater = require('@electron/remote').require('./windows-updater');
|
||||
WindowsUpdater.createRegistryEntries(
|
||||
|
@ -68,7 +68,7 @@ export class DefaultClientHelperWindows implements DCH {
|
|||
},
|
||||
async (err, didMakeDefault) => {
|
||||
if (err) {
|
||||
awaitrequire('@electron/remote').dialog.showMessageBox({
|
||||
await require('@electron/remote').dialog.showMessageBox({
|
||||
type: 'error',
|
||||
buttons: [localized('OK')],
|
||||
message: localized('An error has occurred'),
|
||||
|
@ -78,7 +78,7 @@ export class DefaultClientHelperWindows implements DCH {
|
|||
}
|
||||
|
||||
if (!didMakeDefault) {
|
||||
const { response } = awaitrequire('@electron/remote').dialog.showMessageBox({
|
||||
const { response } = await require('@electron/remote').dialog.showMessageBox({
|
||||
type: 'info',
|
||||
buttons: [localized('Learn More')],
|
||||
defaultId: 1,
|
||||
|
@ -113,12 +113,12 @@ export class DefaultClientHelperLinux implements DCH {
|
|||
);
|
||||
}
|
||||
|
||||
resetURLScheme(scheme: string, callback = (error?: Error) => { }) {
|
||||
resetURLScheme(scheme: string, callback = (error?: Error) => {}) {
|
||||
exec(`xdg-mime default thunderbird.desktop x-scheme-handler/${scheme}`, err =>
|
||||
err ? callback(err) : callback(null)
|
||||
);
|
||||
}
|
||||
registerForURLScheme(scheme: string, callback = (error?: Error) => { }) {
|
||||
registerForURLScheme(scheme: string, callback = (error?: Error) => {}) {
|
||||
exec(`xdg-mime default Mailspring.desktop x-scheme-handler/${scheme}`, err =>
|
||||
err ? callback(err) : callback(null)
|
||||
);
|
||||
|
@ -139,7 +139,7 @@ export class DefaultClientHelperMac implements DCH {
|
|||
fs.exists(secure, exists => (exists ? callback(secure) : callback(insecure)));
|
||||
}
|
||||
|
||||
readDefaults(callback = (result: Error | any, json?: any) => { }) {
|
||||
readDefaults(callback = (result: Error | any, json?: any) => {}) {
|
||||
this.getLaunchServicesPlistPath(plistPath => {
|
||||
const tmpPath = `${plistPath}.${Math.random()}`;
|
||||
exec(`plutil -convert json "${plistPath}" -o "${tmpPath}"`, err => {
|
||||
|
@ -155,7 +155,7 @@ export class DefaultClientHelperMac implements DCH {
|
|||
try {
|
||||
const json = JSON.parse(data.toString());
|
||||
callback(json.LSHandlers, json);
|
||||
fs.unlink(tmpPath, () => { });
|
||||
fs.unlink(tmpPath, () => {});
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ export class DefaultClientHelperMac implements DCH {
|
|||
});
|
||||
}
|
||||
|
||||
writeDefaults(defaults, callback = (error?: Error) => { }) {
|
||||
writeDefaults(defaults, callback = (error?: Error) => {}) {
|
||||
this.getLaunchServicesPlistPath(plistPath => {
|
||||
const tmpPath = `${plistPath}.${Math.random()}`;
|
||||
exec(`plutil -convert json "${plistPath}" -o "${tmpPath}"`, err => {
|
||||
|
@ -183,7 +183,7 @@ export class DefaultClientHelperMac implements DCH {
|
|||
return;
|
||||
}
|
||||
exec(`plutil -convert binary1 "${tmpPath}" -o "${plistPath}"`, () => {
|
||||
fs.unlink(tmpPath, () => { });
|
||||
fs.unlink(tmpPath, () => {});
|
||||
exec(
|
||||
'/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user',
|
||||
registerErr => {
|
||||
|
@ -210,7 +210,7 @@ export class DefaultClientHelperMac implements DCH {
|
|||
});
|
||||
}
|
||||
|
||||
resetURLScheme(scheme: string, callback = (error?: Error) => { }) {
|
||||
resetURLScheme(scheme: string, callback = (error?: Error) => {}) {
|
||||
this.readDefaults(defaults => {
|
||||
// Remove anything already registered for the scheme
|
||||
for (let ii = defaults.length - 1; ii >= 0; ii--) {
|
||||
|
@ -222,7 +222,7 @@ export class DefaultClientHelperMac implements DCH {
|
|||
});
|
||||
}
|
||||
|
||||
registerForURLScheme(scheme: string, callback = (error?: Error) => { }) {
|
||||
registerForURLScheme(scheme: string, callback = (error?: Error) => {}) {
|
||||
this.readDefaults(defaults => {
|
||||
// Remove anything already registered for the scheme
|
||||
for (let ii = defaults.length - 1; ii >= 0; ii--) {
|
||||
|
|
|
@ -156,9 +156,14 @@ class DatabaseStore extends MailspringStore {
|
|||
}
|
||||
|
||||
_prettyConsoleLog(qa) {
|
||||
const darkTheme =
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches,
|
||||
primaryColor = darkTheme ? 'white' : 'black',
|
||||
purpleColor = darkTheme ? 'pink' : 'purple';
|
||||
|
||||
let q = qa.replace(/%/g, '%%');
|
||||
q = `color:black |||%c ${q}`;
|
||||
q = q.replace(/`(\w+)`/g, '||| color:purple |||%c$&||| color:black |||%c');
|
||||
q = `color:${primaryColor} |||%c ${q}`;
|
||||
q = q.replace(/`(\w+)`/g, `||| color:${purpleColor} |||%c$&||| color:${primaryColor} |||%c`);
|
||||
|
||||
const colorRules = {
|
||||
'color:green': [
|
||||
|
@ -184,7 +189,7 @@ class DatabaseStore extends MailspringStore {
|
|||
for (const keyword of colorRules[style]) {
|
||||
q = q.replace(
|
||||
new RegExp(`\\b${keyword}\\b`, 'g'),
|
||||
`||| ${style} |||%c${keyword}||| color:black |||%c`
|
||||
`||| ${style} |||%c${keyword}||| color:${primaryColor} |||%c`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -246,22 +246,55 @@ export class DraftEditingSession extends MailspringStore {
|
|||
}
|
||||
|
||||
validateDraftForSending() {
|
||||
const warnings = [];
|
||||
const errors = [];
|
||||
const allRecipients = [...this._draft.to, ...this._draft.cc, ...this._draft.bcc];
|
||||
const miscWarnings = [];
|
||||
const miscErrors = [];
|
||||
const hasAttachment = this._draft.files && this._draft.files.length > 0;
|
||||
|
||||
if (this._draft.subject.length === 0) {
|
||||
miscWarnings.push(localized('The subject field is blank.'));
|
||||
}
|
||||
|
||||
let cleaned = QuotedHTMLTransformer.removeQuotedHTML(this._draft.body.trim());
|
||||
const sigIndex = cleaned.search(RegExpUtils.mailspringSignatureRegex());
|
||||
cleaned = sigIndex > -1 ? cleaned.substr(0, sigIndex) : cleaned;
|
||||
|
||||
const signatureIndex = cleaned.indexOf('<signature>');
|
||||
if (signatureIndex !== -1) {
|
||||
cleaned = cleaned.substr(0, signatureIndex - 1);
|
||||
}
|
||||
|
||||
if (cleaned.toLowerCase().includes('attach') && !hasAttachment) {
|
||||
miscWarnings.push(localized('The message mentions an attachment but none are attached.'));
|
||||
}
|
||||
|
||||
// Check third party warnings added via Composer extensions
|
||||
for (const extension of ComposerExtensionRegistry.extensions()) {
|
||||
if (!extension.warningsForSending) {
|
||||
continue;
|
||||
}
|
||||
miscWarnings.push(...extension.warningsForSending({ draft: this._draft }));
|
||||
}
|
||||
|
||||
return { miscErrors, miscWarnings };
|
||||
}
|
||||
|
||||
|
||||
validateDraftRecipients() {
|
||||
const recipientWarnings = [];
|
||||
const recipientErrors = [];
|
||||
const allRecipients = [...this._draft.to, ...this._draft.cc, ...this._draft.bcc];
|
||||
|
||||
const allNames = [...Utils.commonlyCapitalizedSalutations];
|
||||
let unnamedRecipientPresent = false;
|
||||
|
||||
for (const contact of allRecipients) {
|
||||
if (!ContactStore.isValidContact(contact)) {
|
||||
errors.push(
|
||||
recipientErrors.push(
|
||||
`${contact.email} is not a valid email address - please remove or edit it before sending.`
|
||||
);
|
||||
}
|
||||
const name = contact.fullName();
|
||||
if (name && name.length && name !== contact.email) {
|
||||
if (name && name.length && name !== contact.email && !this.checkRecipientInWarningBlacklist(contact.email)) {
|
||||
allNames.push(name.toLowerCase()); // ben gotow
|
||||
allNames.push(...name.toLowerCase().split(' ')); // ben, gotow
|
||||
allNames.push(...name.toLowerCase().split('-')); // anne-marie => anne, marie
|
||||
|
@ -276,17 +309,13 @@ export class DraftEditingSession extends MailspringStore {
|
|||
}
|
||||
|
||||
if (allRecipients.length === 0) {
|
||||
errors.push(
|
||||
recipientErrors.push(
|
||||
localized('You need to provide one or more recipients before sending the message.')
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
if (this._draft.subject.length === 0) {
|
||||
warnings.push(localized('The subject field is blank.'));
|
||||
if (recipientErrors.length > 0) {
|
||||
return { errors: recipientErrors, warnings: recipientWarnings };
|
||||
}
|
||||
|
||||
let cleaned = QuotedHTMLTransformer.removeQuotedHTML(this._draft.body.trim());
|
||||
|
@ -298,10 +327,6 @@ export class DraftEditingSession extends MailspringStore {
|
|||
cleaned = cleaned.substr(0, signatureIndex - 1);
|
||||
}
|
||||
|
||||
if (cleaned.toLowerCase().includes('attach') && !hasAttachment) {
|
||||
warnings.push(localized('The message mentions an attachment but none are attached.'));
|
||||
}
|
||||
|
||||
if (!unnamedRecipientPresent) {
|
||||
// https://www.regexpal.com/?fam=99334
|
||||
// note: requires that the name is capitalized, to avoid catching "Hey guys"
|
||||
|
@ -312,7 +337,7 @@ export class DraftEditingSession extends MailspringStore {
|
|||
if (salutation.endsWith('-')) salutation = salutation.substr(0, salutation.length - 1);
|
||||
|
||||
if (!allNames.find(n => n === salutation || (n.length > 1 && salutation.includes(n)))) {
|
||||
warnings.push(
|
||||
recipientWarnings.push(
|
||||
localized(
|
||||
`The message is addressed to a name that doesn't appear to be a recipient ("%@")`,
|
||||
match[1]
|
||||
|
@ -322,17 +347,25 @@ export class DraftEditingSession extends MailspringStore {
|
|||
}
|
||||
}
|
||||
|
||||
// Check third party warnings added via Composer extensions
|
||||
for (const extension of ComposerExtensionRegistry.extensions()) {
|
||||
if (!extension.warningsForSending) {
|
||||
continue;
|
||||
}
|
||||
warnings.push(...extension.warningsForSending({ draft: this._draft }));
|
||||
}
|
||||
|
||||
return { errors, warnings };
|
||||
return { recipientErrors, recipientWarnings };
|
||||
}
|
||||
|
||||
addRecipientsToWarningBlacklist() {
|
||||
const allRecipients = [...this._draft.to, ...this._draft.cc, ...this._draft.bcc];
|
||||
const allRecipientEmails = allRecipients.map(contact => contact.email);
|
||||
let blacklist = JSON.parse(localStorage.getItem("recipientWarningBlacklist"));
|
||||
if (blacklist === null) blacklist = [];
|
||||
blacklist.push(...allRecipientEmails);
|
||||
localStorage.setItem("recipientWarningBlacklist", JSON.stringify(blacklist));
|
||||
}
|
||||
|
||||
checkRecipientInWarningBlacklist(email) {
|
||||
const blacklist = JSON.parse(localStorage.getItem("recipientWarningBlacklist"));
|
||||
if (blacklist && blacklist.includes(email)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// This function makes sure the draft is attached to a valid account, and changes
|
||||
// it's accountId if the from address does not match the account for the from
|
||||
// address.
|
||||
|
|
|
@ -105,7 +105,14 @@ export class MailsyncProcess extends EventEmitter {
|
|||
|
||||
_showStatusWindow(mode) {
|
||||
if (this._win) return;
|
||||
const { BrowserWindow } = require('@electron/remote');
|
||||
|
||||
let BrowserWindow;
|
||||
if (process.type === 'renderer') {
|
||||
BrowserWindow = require('@electron/remote').BrowserWindow;
|
||||
} else {
|
||||
BrowserWindow = require('electron').BrowserWindow;
|
||||
}
|
||||
|
||||
this._win = new BrowserWindow({
|
||||
width: 350,
|
||||
height: 108,
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 1,007 B |
Binary file not shown.
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 944 B |
Binary file not shown.
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 1.4 KiB |
|
@ -185,10 +185,7 @@ body.platform-win32 {
|
|||
position: relative;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: @spacing-standard;
|
||||
margin-right: @spacing-standard;
|
||||
margin-left: @spacing-standard;
|
||||
margin: 0;
|
||||
width: initial;
|
||||
max-width: calc(~'100% - 30px');
|
||||
|
||||
|
@ -277,4 +274,52 @@ body.platform-win32 {
|
|||
background-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.resizer {
|
||||
align-items: center;
|
||||
bottom: 0;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
cursor: nwse-resize;
|
||||
display: flex;
|
||||
height: 20px;
|
||||
justify-content: center;
|
||||
opacity: .3;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
z-index: 9;
|
||||
|
||||
// Thanks: https://css.gg/arrows-expand-left
|
||||
.gg-arrows-expand-left {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
transform: scale(0.9);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
box-shadow:
|
||||
6px 6px 0 -4px,
|
||||
-6px -6px 0 -4px
|
||||
}
|
||||
.gg-arrows-expand-left::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 22px;
|
||||
top: -4px;
|
||||
left: 6px;
|
||||
transform: rotate(-45deg);
|
||||
border-top: 9px solid;
|
||||
border-bottom: 9px solid
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &[data-resizing] {
|
||||
.resizer {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
mailsync
2
mailsync
|
@ -1 +1 @@
|
|||
Subproject commit 8007d5f832d831ea927c46bf777d8167317d8bcb
|
||||
Subproject commit ee58f8a6178bbbb3ae86be413e29141992cf2b7a
|
Loading…
Add table
Reference in a new issue