diff --git a/cmd/install.go b/cmd/install.go index ed78a71f..d354744f 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -136,7 +136,13 @@ func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt, idempo "Welcome to listmonk", "No Reply ", `

Hi {{ .Subscriber.FirstName }}!

- This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.`, +

This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.

+

Here is a tracked link.

+

Use the link icon in the editor toolbar or when writing raw HTML or Markdown, + simply suffix @TrackLink to the end of a URL to turn it into a tracking link. Example:

+
<a href="https:/‌/listmonk.app@TrackLink"></a>
+

For help, refer to the documentation.

+ `, nil, "richtext", nil, diff --git a/frontend/src/assets/style.scss b/frontend/src/assets/style.scss index b11c90fc..1c9c637b 100644 --- a/frontend/src/assets/style.scss +++ b/frontend/src/assets/style.scss @@ -225,6 +225,17 @@ body.is-noscroll { border: 0; } } + +.tox-track-link { + display: block !important; + cursor: pointer !important; + + margin: 5px 0 10px 0 !important; + input { + margin-right: 5px !important; + } +} + .plain-editor textarea { height: 65vh; } diff --git a/frontend/src/components/Editor.vue b/frontend/src/components/Editor.vue index 3cf0f735..547e92b5 100644 --- a/frontend/src/components/Editor.vue +++ b/frontend/src/components/Editor.vue @@ -148,6 +148,7 @@ export default { isReady: false, isRichtextReady: false, richtextConf: {}, + isTrackLink: false, form: { body: '', format: this.contentType, @@ -174,7 +175,18 @@ export default { const { lang } = this.serverConfig; this.richtextConf = { + init_instance_callback: () => { this.isReady = true; }, + urlconverter_callback: this.onEditorURLConvert, + + setup: (editor) => { + editor.on('init', () => { + this.onEditorDialogOpen(editor); + }); + }, + min_height: 500, + entity_encoding: 'raw', + convert_urls: true, plugins: [ 'autoresize', 'autolink', 'charmap', 'code', 'emoticons', 'fullscreen', 'help', 'hr', 'image', 'imagetools', 'link', 'lists', 'paste', 'searchreplace', @@ -194,15 +206,14 @@ export default { table, td { border-color: #ccc;} `, + language: LANGS[lang] || null, + language_url: LANGS[lang] ? `${uris.static}/tinymce/lang/${LANGS[lang]}.js` : null, + file_picker_types: 'image', file_picker_callback: (callback) => { this.isMediaVisible = true; this.runTinyMceImageCallback = callback; }, - init_instance_callback: () => { this.isReady = true; }, - - language: LANGS[lang] || null, - language_url: LANGS[lang] ? `${uris.static}/tinymce/lang/${LANGS[lang]}.js` : null, }; this.isRichtextReady = true; @@ -258,6 +269,72 @@ export default { ); }, + onEditorURLConvert(url) { + let u = url; + if (this.isTrackLink) { + u = `${u}@TrackLink`; + } + + this.isTrackLink = false; + return u; + }, + + onEditorDialogOpen(editor) { + const ed = editor; + const oldEd = ed.windowManager.open; + const self = this; + + ed.windowManager.open = (t, r) => { + const isOK = t.initialData && 'url' in t.initialData && 'anchor' in t.initialData; + + // Not the link modal. + if (!isOK) { + return oldEd.apply(this, [t, r]); + } + + // If an existing link is being edited, check for the tracking flag `@TrackLink` at the end + // of the url. Remove that from the URL and instead check the checkbox. + let checked = false; + if (!t.initialData.link !== '') { + const t2 = t; + const url = t2.initialData.url.value.replace(/@TrackLink$/, ''); + + if (t2.initialData.url.value !== url) { + t2.initialData.url.value = url; + checked = true; + } + } + + // Execute the modal. + const modal = oldEd.apply(this, [t, r]); + + // Is it the link dialog? + if (isOK) { + // Insert tracking checkbox. + const c = document.createElement('input'); + c.setAttribute('type', 'checkbox'); + + if (checked) { + c.setAttribute('checked', checked); + } + + // Store the checkbox's state in the Vue instance to pick up from + // the TinyMCE link conversion callback. + c.onchange = (e) => { + self.isTrackLink = e.target.checked; + }; + + const l = document.createElement('label'); + l.appendChild(c); + l.appendChild(document.createTextNode('Track link?')); + l.classList.add('tox-label', 'tox-track-link'); + + document.querySelector('.tox-form__controls-h-stack .tox-control-wrap').appendChild(l); + } + return modal; + }; + }, + onEditorChange() { if (!this.isReady) { return; diff --git a/i18n/cs-cz.json b/i18n/cs-cz.json index 6afa62e9..fb7a7f26 100644 --- a/i18n/cs-cz.json +++ b/i18n/cs-cz.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "E-maily", "campaigns.testSent": "Testovací zpráva odeslána", "campaigns.timestamps": "Časová razítka", + "campaigns.trackLink": "Track link", "campaigns.views": "Pohledy", "dashboard.campaignViews": "Pohledy na kampaň", "dashboard.linkClicks": "Klepnutí na odkaz", diff --git a/i18n/de.json b/i18n/de.json index 252e6b0f..9e1166bf 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "E-Mails", "campaigns.testSent": "Testnachricht gesendet", "campaigns.timestamps": "Zeitstempel", + "campaigns.trackLink": "Track link", "campaigns.views": "Ansichten", "dashboard.campaignViews": "Kampagnenansichten", "dashboard.linkClicks": "Linkklicks", diff --git a/i18n/en.json b/i18n/en.json index 600275ed..0d3b8524 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "E-mails", "campaigns.testSent": "Test message sent", "campaigns.timestamps": "Timestamps", + "campaigns.trackLink": "Track link", "campaigns.views": "Views", "dashboard.campaignViews": "Campaign views", "dashboard.linkClicks": "Link clicks", diff --git a/i18n/es.json b/i18n/es.json index 8411e3eb..1cd6bc97 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "Correos electrónicos", "campaigns.testSent": "Mensaje de prueba enviado", "campaigns.timestamps": "Marca de timepo", + "campaigns.trackLink": "Track link", "campaigns.views": "Vistas", "dashboard.campaignViews": "Vista de campañas", "dashboard.linkClicks": "Vinculos cliqueados", diff --git a/i18n/fr.json b/i18n/fr.json index f0508a9a..1858bf25 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "Emails de test", "campaigns.testSent": "Message de test envoyé", "campaigns.timestamps": "Horodatages", + "campaigns.trackLink": "Track link", "campaigns.views": "Vues", "dashboard.campaignViews": "vues de campagne", "dashboard.linkClicks": "clics sur liens", diff --git a/i18n/it.json b/i18n/it.json index b1300443..87410018 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "Emails di prova", "campaigns.testSent": "Messaggio di prova inviato", "campaigns.timestamps": "Marcatura temporale ", + "campaigns.trackLink": "Track link", "campaigns.views": "Visualizzazioni", "dashboard.campaignViews": "Visualizzazioni della campagna", "dashboard.linkClicks": "Clic sui link", diff --git a/i18n/ml.json b/i18n/ml.json index a0ab14e7..c613f360 100644 --- a/i18n/ml.json +++ b/i18n/ml.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "ഈ-മെയിലുകൾ", "campaigns.testSent": "ടെസ്റ്റ് സന്ദേശം അയച്ചു", "campaigns.timestamps": "സമയം", + "campaigns.trackLink": "Track link", "campaigns.views": "കാഴ്ചകൾ", "dashboard.campaignViews": "ക്യാമ്പേയ്ൻ കാഴ്ചകൾ", "dashboard.linkClicks": "കണ്ണിയിലെ ക്ലിക്കുകൾ", diff --git a/i18n/pl.json b/i18n/pl.json index 3df6f2e2..5046daa0 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "E-maile", "campaigns.testSent": "Wiadomość testowa wysłana", "campaigns.timestamps": "Sygnatury czasowe", + "campaigns.trackLink": "Track link", "campaigns.views": "Wyświetlenia", "dashboard.campaignViews": "Wyświetlenia kampanii", "dashboard.linkClicks": "Kliknięcia linków", diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index d3eb74f4..2059394b 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "E-mails", "campaigns.testSent": "Mensagem de teste enviada", "campaigns.timestamps": "Data e hora", + "campaigns.trackLink": "Track link", "campaigns.views": "Visualizações", "dashboard.campaignViews": "Visualizações da campanha", "dashboard.linkClicks": "Links clicados", diff --git a/i18n/pt.json b/i18n/pt.json index f3b0fb4e..c873ad23 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "E-mails", "campaigns.testSent": "Mensagem de teste enviada", "campaigns.timestamps": "Carimbo de hora", + "campaigns.trackLink": "Track link", "campaigns.views": "Visualizações", "dashboard.campaignViews": "Vista de campanhas", "dashboard.linkClicks": "Cliques nos links", diff --git a/i18n/ro.json b/i18n/ro.json index b9e9fdd4..bfdea69b 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "Emailuri", "campaigns.testSent": "Mesaju de test a fost trimis", "campaigns.timestamps": "Marcaje de timp", + "campaigns.trackLink": "Track link", "campaigns.views": "Vizualizări", "dashboard.campaignViews": "Vizualizări ale campaniei", "dashboard.linkClicks": "Clickuri pe link", diff --git a/i18n/ru.json b/i18n/ru.json index c35d6bfa..fa4f9e32 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "E-mails", "campaigns.testSent": "Тестовое сообщение отправлено", "campaigns.timestamps": "Метки времени", + "campaigns.trackLink": "Track link", "campaigns.views": "Просмотры", "dashboard.campaignViews": "Просмотров компании", "dashboard.linkClicks": "Кликов по ссылкам", diff --git a/i18n/tr.json b/i18n/tr.json index ce96387a..b9b958fe 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -78,6 +78,7 @@ "campaigns.testEmails": "E-postalar", "campaigns.testSent": "Test mesajı gönderildi", "campaigns.timestamps": "Zaman etiketi", + "campaigns.trackLink": "Track link", "campaigns.views": "Görüntülenme", "dashboard.campaignViews": "Kampanya görüntülenme Sayısı", "dashboard.linkClicks": "Linklerin tıklanması", diff --git a/models/models.go b/models/models.go index 1f5ffa92..29a1f2ec 100644 --- a/models/models.go +++ b/models/models.go @@ -80,15 +80,23 @@ type regTplFunc struct { replace string } -// Regular expression for matching {{ Track "http://link.com" }} in the template -// and substituting it with {{ Track "http://link.com" .Campaign.UUID .Subscriber.UUID }} -// before compilation. This string gimmick is to make linking easier for users. var regTplFuncs = []regTplFunc{ - regTplFunc{ + // Convert the shorthand https://google.com@TrackLink to {{ TrackLink ... }}. + // This is for WYSIWYG editors that encode and break quotes {{ "" }} when inserted + // inside . + { + regExp: regexp.MustCompile(`(https?://.+?)@TrackLink`), + replace: `{{ TrackLink "$1" . }}`, + }, + + // Regular expression for matching {{ TrackLink "http://link.com" }} in the template + // and substituting it with {{ Track "http://link.com" . }} (the dot context) + // before compilation. This is to make linking easier for users. + { regExp: regexp.MustCompile("{{(\\s+)?TrackLink\\s+?(\"|`)(.+?)(\"|`)(\\s+)?}}"), replace: `{{ TrackLink "$3" . }}`, }, - regTplFunc{ + { regExp: regexp.MustCompile(`{{(\s+)?(TrackView|UnsubscribeURL|OptinURL|MessageURL)(\s+)?}}`), replace: `{{ $2 . }}`, }, diff --git a/static/email-templates/default.tpl b/static/email-templates/default.tpl index 72d5da96..ce8baaa1 100644 --- a/static/email-templates/default.tpl +++ b/static/email-templates/default.tpl @@ -15,6 +15,20 @@ color: #444; } + pre { + background: #f4f4f4f4; + padding: 2px; + } + + table { + width: 100%; + border: 1px solid #ddd; + } + table td { + border-color: #ddd; + padding: 5px; + } + .wrap { background-color: #fff; padding: 30px;