Преглед на файлове

Introduce `@TrackLink` shorthand for generating tracking links.

The default `{{ TrackLink "https://listmonk.app" }}` template function
is clumsy to write and does breaks WYSIWYG editors and HTML syntax
highlighting because of the quotes. The new syntax doesn't break HTML
and is easier to write.

Eg: `<a href="https://listmonk.app@TrackLink">Link</a>`

- Introduce @TrackLink shorthand.
- Add first-class support for tracking links in the WYSIWYG (TinyMCE)
  editor by introducing an on/off checkbox on the link dialog.
- Improve default dummy campaign content to highlight this.
Kailash Nadh преди 3 години
родител
ревизия
d86438bde9
променени са 18 файла, в които са добавени 139 реда и са изтрити 10 реда
  1. 7 1
      cmd/install.go
  2. 11 0
      frontend/src/assets/style.scss
  3. 81 4
      frontend/src/components/Editor.vue
  4. 1 0
      i18n/cs-cz.json
  5. 1 0
      i18n/de.json
  6. 1 0
      i18n/en.json
  7. 1 0
      i18n/es.json
  8. 1 0
      i18n/fr.json
  9. 1 0
      i18n/it.json
  10. 1 0
      i18n/ml.json
  11. 1 0
      i18n/pl.json
  12. 1 0
      i18n/pt-BR.json
  13. 1 0
      i18n/pt.json
  14. 1 0
      i18n/ro.json
  15. 1 0
      i18n/ru.json
  16. 1 0
      i18n/tr.json
  17. 13 5
      models/models.go
  18. 14 0
      static/email-templates/default.tpl

+ 7 - 1
cmd/install.go

@@ -136,7 +136,13 @@ func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt, idempo
 		"Welcome to listmonk",
 		"Welcome to listmonk",
 		"No Reply <noreply@yoursite.com>",
 		"No Reply <noreply@yoursite.com>",
 		`<h3>Hi {{ .Subscriber.FirstName }}!</h3>
 		`<h3>Hi {{ .Subscriber.FirstName }}!</h3>
-			This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.`,
+		<p>This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.</p>
+		<p>Here is a <a href="https://listmonk.app@TrackLink">tracked link</a>.</p>
+		<p>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:</p>
+		<pre>&lt;a href=&quot;https:/&zwnj;/listmonk.app&#064;TrackLink&quot;&gt;&lt;/a&gt;</pre>
+		<p>For help, refer to the <a href="https://listmonk.app/docs">documentation</a>.</p>
+		`,
 		nil,
 		nil,
 		"richtext",
 		"richtext",
 		nil,
 		nil,

+ 11 - 0
frontend/src/assets/style.scss

@@ -225,6 +225,17 @@ body.is-noscroll {
     border: 0;
     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 {
   .plain-editor textarea {
     height: 65vh;
     height: 65vh;
   }
   }

+ 81 - 4
frontend/src/components/Editor.vue

@@ -148,6 +148,7 @@ export default {
       isReady: false,
       isReady: false,
       isRichtextReady: false,
       isRichtextReady: false,
       richtextConf: {},
       richtextConf: {},
+      isTrackLink: false,
       form: {
       form: {
         body: '',
         body: '',
         format: this.contentType,
         format: this.contentType,
@@ -174,7 +175,18 @@ export default {
       const { lang } = this.serverConfig;
       const { lang } = this.serverConfig;
 
 
       this.richtextConf = {
       this.richtextConf = {
+        init_instance_callback: () => { this.isReady = true; },
+        urlconverter_callback: this.onEditorURLConvert,
+
+        setup: (editor) => {
+          editor.on('init', () => {
+            this.onEditorDialogOpen(editor);
+          });
+        },
+
         min_height: 500,
         min_height: 500,
+        entity_encoding: 'raw',
+        convert_urls: true,
         plugins: [
         plugins: [
           'autoresize', 'autolink', 'charmap', 'code', 'emoticons', 'fullscreen', 'help',
           'autoresize', 'autolink', 'charmap', 'code', 'emoticons', 'fullscreen', 'help',
           'hr', 'image', 'imagetools', 'link', 'lists', 'paste', 'searchreplace',
           'hr', 'image', 'imagetools', 'link', 'lists', 'paste', 'searchreplace',
@@ -194,15 +206,14 @@ export default {
           table, td { border-color: #ccc;}
           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_types: 'image',
         file_picker_callback: (callback) => {
         file_picker_callback: (callback) => {
           this.isMediaVisible = true;
           this.isMediaVisible = true;
           this.runTinyMceImageCallback = callback;
           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;
       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() {
     onEditorChange() {
       if (!this.isReady) {
       if (!this.isReady) {
         return;
         return;

+ 1 - 0
i18n/cs-cz.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "E-maily",
     "campaigns.testEmails": "E-maily",
     "campaigns.testSent": "Testovací zpráva odeslána",
     "campaigns.testSent": "Testovací zpráva odeslána",
     "campaigns.timestamps": "Časová razítka",
     "campaigns.timestamps": "Časová razítka",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Pohledy",
     "campaigns.views": "Pohledy",
     "dashboard.campaignViews": "Pohledy na kampaň",
     "dashboard.campaignViews": "Pohledy na kampaň",
     "dashboard.linkClicks": "Klepnutí na odkaz",
     "dashboard.linkClicks": "Klepnutí na odkaz",

+ 1 - 0
i18n/de.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "E-Mails",
     "campaigns.testEmails": "E-Mails",
     "campaigns.testSent": "Testnachricht gesendet",
     "campaigns.testSent": "Testnachricht gesendet",
     "campaigns.timestamps": "Zeitstempel",
     "campaigns.timestamps": "Zeitstempel",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Ansichten",
     "campaigns.views": "Ansichten",
     "dashboard.campaignViews": "Kampagnenansichten",
     "dashboard.campaignViews": "Kampagnenansichten",
     "dashboard.linkClicks": "Linkklicks",
     "dashboard.linkClicks": "Linkklicks",

+ 1 - 0
i18n/en.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "E-mails",
     "campaigns.testEmails": "E-mails",
     "campaigns.testSent": "Test message sent",
     "campaigns.testSent": "Test message sent",
     "campaigns.timestamps": "Timestamps",
     "campaigns.timestamps": "Timestamps",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Views",
     "campaigns.views": "Views",
     "dashboard.campaignViews": "Campaign views",
     "dashboard.campaignViews": "Campaign views",
     "dashboard.linkClicks": "Link clicks",
     "dashboard.linkClicks": "Link clicks",

+ 1 - 0
i18n/es.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "Correos electrónicos",
     "campaigns.testEmails": "Correos electrónicos",
     "campaigns.testSent": "Mensaje de prueba enviado",
     "campaigns.testSent": "Mensaje de prueba enviado",
     "campaigns.timestamps": "Marca de timepo",
     "campaigns.timestamps": "Marca de timepo",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Vistas",
     "campaigns.views": "Vistas",
     "dashboard.campaignViews": "Vista de campañas",
     "dashboard.campaignViews": "Vista de campañas",
     "dashboard.linkClicks": "Vinculos cliqueados",
     "dashboard.linkClicks": "Vinculos cliqueados",

+ 1 - 0
i18n/fr.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "Emails de test",
     "campaigns.testEmails": "Emails de test",
     "campaigns.testSent": "Message de test envoyé",
     "campaigns.testSent": "Message de test envoyé",
     "campaigns.timestamps": "Horodatages",
     "campaigns.timestamps": "Horodatages",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Vues",
     "campaigns.views": "Vues",
     "dashboard.campaignViews": "vues de campagne",
     "dashboard.campaignViews": "vues de campagne",
     "dashboard.linkClicks": "clics sur liens",
     "dashboard.linkClicks": "clics sur liens",

+ 1 - 0
i18n/it.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "Emails di prova",
     "campaigns.testEmails": "Emails di prova",
     "campaigns.testSent": "Messaggio di prova inviato",
     "campaigns.testSent": "Messaggio di prova inviato",
     "campaigns.timestamps": "Marcatura temporale ",
     "campaigns.timestamps": "Marcatura temporale ",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Visualizzazioni",
     "campaigns.views": "Visualizzazioni",
     "dashboard.campaignViews": "Visualizzazioni della campagna",
     "dashboard.campaignViews": "Visualizzazioni della campagna",
     "dashboard.linkClicks": "Clic sui link",
     "dashboard.linkClicks": "Clic sui link",

+ 1 - 0
i18n/ml.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "ഈ-മെയിലുകൾ",
     "campaigns.testEmails": "ഈ-മെയിലുകൾ",
     "campaigns.testSent": "ടെസ്റ്റ് സന്ദേശം അയച്ചു",
     "campaigns.testSent": "ടെസ്റ്റ് സന്ദേശം അയച്ചു",
     "campaigns.timestamps": "സമയം",
     "campaigns.timestamps": "സമയം",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "കാഴ്ചകൾ",
     "campaigns.views": "കാഴ്ചകൾ",
     "dashboard.campaignViews": "ക്യാമ്പേയ്ൻ കാഴ്ചകൾ",
     "dashboard.campaignViews": "ക്യാമ്പേയ്ൻ കാഴ്ചകൾ",
     "dashboard.linkClicks": "കണ്ണിയിലെ ക്ലിക്കുകൾ",
     "dashboard.linkClicks": "കണ്ണിയിലെ ക്ലിക്കുകൾ",

+ 1 - 0
i18n/pl.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "E-maile",
     "campaigns.testEmails": "E-maile",
     "campaigns.testSent": "Wiadomość testowa wysłana",
     "campaigns.testSent": "Wiadomość testowa wysłana",
     "campaigns.timestamps": "Sygnatury czasowe",
     "campaigns.timestamps": "Sygnatury czasowe",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Wyświetlenia",
     "campaigns.views": "Wyświetlenia",
     "dashboard.campaignViews": "Wyświetlenia kampanii",
     "dashboard.campaignViews": "Wyświetlenia kampanii",
     "dashboard.linkClicks": "Kliknięcia linków",
     "dashboard.linkClicks": "Kliknięcia linków",

+ 1 - 0
i18n/pt-BR.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "E-mails",
     "campaigns.testEmails": "E-mails",
     "campaigns.testSent": "Mensagem de teste enviada",
     "campaigns.testSent": "Mensagem de teste enviada",
     "campaigns.timestamps": "Data e hora",
     "campaigns.timestamps": "Data e hora",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Visualizações",
     "campaigns.views": "Visualizações",
     "dashboard.campaignViews": "Visualizações da campanha",
     "dashboard.campaignViews": "Visualizações da campanha",
     "dashboard.linkClicks": "Links clicados",
     "dashboard.linkClicks": "Links clicados",

+ 1 - 0
i18n/pt.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "E-mails",
     "campaigns.testEmails": "E-mails",
     "campaigns.testSent": "Mensagem de teste enviada",
     "campaigns.testSent": "Mensagem de teste enviada",
     "campaigns.timestamps": "Carimbo de hora",
     "campaigns.timestamps": "Carimbo de hora",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Visualizações",
     "campaigns.views": "Visualizações",
     "dashboard.campaignViews": "Vista de campanhas",
     "dashboard.campaignViews": "Vista de campanhas",
     "dashboard.linkClicks": "Cliques nos links",
     "dashboard.linkClicks": "Cliques nos links",

+ 1 - 0
i18n/ro.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "Emailuri",
     "campaigns.testEmails": "Emailuri",
     "campaigns.testSent": "Mesaju de test a fost trimis",
     "campaigns.testSent": "Mesaju de test a fost trimis",
     "campaigns.timestamps": "Marcaje de timp",
     "campaigns.timestamps": "Marcaje de timp",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Vizualizări",
     "campaigns.views": "Vizualizări",
     "dashboard.campaignViews": "Vizualizări ale campaniei",
     "dashboard.campaignViews": "Vizualizări ale campaniei",
     "dashboard.linkClicks": "Clickuri pe link",
     "dashboard.linkClicks": "Clickuri pe link",

+ 1 - 0
i18n/ru.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "E-mails",
     "campaigns.testEmails": "E-mails",
     "campaigns.testSent": "Тестовое сообщение отправлено",
     "campaigns.testSent": "Тестовое сообщение отправлено",
     "campaigns.timestamps": "Метки времени",
     "campaigns.timestamps": "Метки времени",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Просмотры",
     "campaigns.views": "Просмотры",
     "dashboard.campaignViews": "Просмотров компании",
     "dashboard.campaignViews": "Просмотров компании",
     "dashboard.linkClicks": "Кликов по ссылкам",
     "dashboard.linkClicks": "Кликов по ссылкам",

+ 1 - 0
i18n/tr.json

@@ -78,6 +78,7 @@
     "campaigns.testEmails": "E-postalar",
     "campaigns.testEmails": "E-postalar",
     "campaigns.testSent": "Test mesajı gönderildi",
     "campaigns.testSent": "Test mesajı gönderildi",
     "campaigns.timestamps": "Zaman etiketi",
     "campaigns.timestamps": "Zaman etiketi",
+    "campaigns.trackLink": "Track link",
     "campaigns.views": "Görüntülenme",
     "campaigns.views": "Görüntülenme",
     "dashboard.campaignViews": "Kampanya görüntülenme Sayısı",
     "dashboard.campaignViews": "Kampanya görüntülenme Sayısı",
     "dashboard.linkClicks": "Linklerin tıklanması",
     "dashboard.linkClicks": "Linklerin tıklanması",

+ 13 - 5
models/models.go

@@ -80,15 +80,23 @@ type regTplFunc struct {
 	replace string
 	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{
 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 <a href="{{ TrackLink "https://these-quotes-break" }}>.
+	{
+		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+)?}}"),
 		regExp:  regexp.MustCompile("{{(\\s+)?TrackLink\\s+?(\"|`)(.+?)(\"|`)(\\s+)?}}"),
 		replace: `{{ TrackLink "$3" . }}`,
 		replace: `{{ TrackLink "$3" . }}`,
 	},
 	},
-	regTplFunc{
+	{
 		regExp:  regexp.MustCompile(`{{(\s+)?(TrackView|UnsubscribeURL|OptinURL|MessageURL)(\s+)?}}`),
 		regExp:  regexp.MustCompile(`{{(\s+)?(TrackView|UnsubscribeURL|OptinURL|MessageURL)(\s+)?}}`),
 		replace: `{{ $2 . }}`,
 		replace: `{{ $2 . }}`,
 	},
 	},

+ 14 - 0
static/email-templates/default.tpl

@@ -15,6 +15,20 @@
                 color: #444;
                 color: #444;
             }
             }
 
 
+            pre {
+                background: #f4f4f4f4;
+                padding: 2px;
+            }
+
+            table {
+                width: 100%;
+                border: 1px solid #ddd;
+            }
+            table td {
+                border-color: #ddd;
+                padding: 5px;
+            }
+
             .wrap {
             .wrap {
                 background-color: #fff;
                 background-color: #fff;
                 padding: 30px;
                 padding: 30px;