From 60e992e93130050d6d08da75383e2fdae455262a Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Tue, 23 Jan 2024 14:33:24 +1300 Subject: [PATCH 01/20] FEATURE: chat integration and open link in new window Added integration with Discourse Chat to allow creating a link within the options of a chat window Open plain meeting links in a new browser tab rather than overwriting the current tab Added settings for the new chat button integration --- .eslintrc.cjs | 2 +- .../components/modal/insert-jitsi.gjs | 59 +++++++++++-------- .../discourse/initializers/insert-jitsi.js | 42 ++++++++++++- settings.yml | 10 ++++ 4 files changed, 85 insertions(+), 28 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index ee89c90..be1a9f3 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1 +1 @@ -module.exports = require("@discourse/lint-configs/eslint"); \ No newline at end of file +module.exports = require("@discourse/lint-configs/eslint"); diff --git a/javascripts/discourse/components/modal/insert-jitsi.gjs b/javascripts/discourse/components/modal/insert-jitsi.gjs index 7aef283..ba78f7f 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.gjs +++ b/javascripts/discourse/components/modal/insert-jitsi.gjs @@ -31,9 +31,17 @@ export default class InsertJitsi extends Component { insert() { const btnTxt = this.buttonText ? ` label="${this.buttonText}"` : ""; const roomID = this.jitsiRoom || this.randomID(); - const text = `[wrap=discourse-jitsi room="${roomID}"${btnTxt} mobileIframe="${this.mobileIframe}" desktopIframe="${this.desktopIframe}"][/wrap]`; - this.args.model.toolbarEvent.addText(text); + let text; + + if (this.args.model.plainText) { + const domain = settings.meet_jitsi_domain; + text = `https://${domain}/${roomID}`; + } else { + text = `[wrap=discourse-jitsi room="${roomID}"${btnTxt} mobileIframe="${this.mobileIframe}" desktopIframe="${this.desktopIframe}"][/wrap]`; + } + + this.args.model.insertMeeting(text); this.args.closeModal(); } @@ -64,31 +72,34 @@ export default class InsertJitsi extends Component { }} -
- - -
+ {{#unless @model.plainText}} -
- -
+
+ + +
-
- -
- +
+ +
+
+ +
+ + {{/unless}} + <:footer> { + modal.show(InsertJitsi, { + model: { + insertMeeting: (text) => { + // Get the active channel ensuring one is present + const activeChannel = chat.activeChannel; + if (activeChannel === null) { + return; + } + + // Append the meeting link to the draft + activeChannel.draft.message += text; + }, + plainText: true, + }, + }); + }, + }); + } + if (settings.show_in_options_dropdown) { if (!settings.only_available_to_staff || currentUser?.staff) { api.addComposerToolbarPopupMenuOption({ icon: settings.button_icon, label: themePrefix("composer_title"), action: (toolbarEvent) => { - modal.show(InsertJitsi, { model: { toolbarEvent } }); + modal.show(InsertJitsi, { + model: { insertMeeting: toolbarEvent.addText }, + }); }, }); } @@ -101,7 +135,9 @@ export default { group: "insertions", icon: settings.button_icon, perform: (toolbarEvent) => - modal.show(InsertJitsi, { model: { toolbarEvent } }), + modal.show(InsertJitsi, { + model: { insertMeeting: toolbarEvent.addText }, + }), }); }); } diff --git a/settings.yml b/settings.yml index 4caeb2e..8cd4b53 100644 --- a/settings.yml +++ b/settings.yml @@ -12,6 +12,16 @@ jitsi_script_src: button_icon: default: "video" description: "Enter the name of the FontAwesome 5 icon to display in the Jitsi button. " +chat_button: + default: true + description: "Integrate with Discourse Chat" +chat_button_position: + description: "Position of the button within the chat window" + default: dropdown + type: enum + choices: + - dropdown + - inline svg_icons: default: "video" type: "list" From 34190babefadc56f851128a348b8d6f01af232f5 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Tue, 23 Jan 2024 18:57:36 +1300 Subject: [PATCH 02/20] FEATURE: default settings for iframe modes --- javascripts/discourse/components/modal/insert-jitsi.gjs | 4 ++-- settings.yml | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/javascripts/discourse/components/modal/insert-jitsi.gjs b/javascripts/discourse/components/modal/insert-jitsi.gjs index ba78f7f..58ecd2d 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.gjs +++ b/javascripts/discourse/components/modal/insert-jitsi.gjs @@ -10,8 +10,8 @@ import TextField from "discourse/components/text-field"; import i18n from "discourse-common/helpers/i18n"; export default class InsertJitsi extends Component { - @tracked mobileIframe = true; - @tracked desktopIframe = true; + @tracked mobileIframe = settings.default_mobile_iframe; + @tracked desktopIframe = settings.default_desktop_iframe; @tracked jitsiRoom = ""; @tracked buttonText = ""; diff --git a/settings.yml b/settings.yml index 8cd4b53..9ff4b5a 100644 --- a/settings.yml +++ b/settings.yml @@ -15,6 +15,12 @@ button_icon: chat_button: default: true description: "Integrate with Discourse Chat" +default_mobile_iframe: + default: true + description: "Enable iframe mode by default for mobile" +default_desktop_iframe: + default: true + description: "Enable iframe mode by default for desktop" chat_button_position: description: "Position of the button within the chat window" default: dropdown From f8537fd2484c1f8098b423e96f1e3c9ab0697986 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Tue, 23 Jan 2024 14:33:24 +1300 Subject: [PATCH 03/20] FEATURE: chat integration and open link in new window Added integration with Discourse Chat to allow creating a link within the options of a chat window Open plain meeting links in a new browser tab rather than overwriting the current tab Added settings for the new chat button integration --- .eslintrc.cjs | 2 +- .../components/modal/insert-jitsi.gjs | 59 ++++++++------- .../discourse/initializers/insert-jitsi.js | 72 +++++++++++++------ settings.yml | 10 +++ 4 files changed, 98 insertions(+), 45 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index ee89c90..be1a9f3 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1 +1 @@ -module.exports = require("@discourse/lint-configs/eslint"); \ No newline at end of file +module.exports = require("@discourse/lint-configs/eslint"); diff --git a/javascripts/discourse/components/modal/insert-jitsi.gjs b/javascripts/discourse/components/modal/insert-jitsi.gjs index 7aef283..ba78f7f 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.gjs +++ b/javascripts/discourse/components/modal/insert-jitsi.gjs @@ -31,9 +31,17 @@ export default class InsertJitsi extends Component { insert() { const btnTxt = this.buttonText ? ` label="${this.buttonText}"` : ""; const roomID = this.jitsiRoom || this.randomID(); - const text = `[wrap=discourse-jitsi room="${roomID}"${btnTxt} mobileIframe="${this.mobileIframe}" desktopIframe="${this.desktopIframe}"][/wrap]`; - this.args.model.toolbarEvent.addText(text); + let text; + + if (this.args.model.plainText) { + const domain = settings.meet_jitsi_domain; + text = `https://${domain}/${roomID}`; + } else { + text = `[wrap=discourse-jitsi room="${roomID}"${btnTxt} mobileIframe="${this.mobileIframe}" desktopIframe="${this.desktopIframe}"][/wrap]`; + } + + this.args.model.insertMeeting(text); this.args.closeModal(); } @@ -64,31 +72,34 @@ export default class InsertJitsi extends Component { }} -
- - -
+ {{#unless @model.plainText}} -
- -
+
+ + +
-
- -
- +
+ +
+
+ +
+ + {{/unless}} + <:footer> { + modal.show(InsertJitsi, { + model: { + insertMeeting: (text) => { + // Get the active channel ensuring one is present + const activeChannel = chat.activeChannel; + if (activeChannel === null) { + return; + } + + // Append the meeting link to the draft + activeChannel.draft.message += text; + }, + plainText: true, + }, + }); + }, + }); + } + if (settings.show_in_options_dropdown) { - if (!settings.only_available_to_staff || currentUser?.staff) { - api.addComposerToolbarPopupMenuOption({ - icon: settings.button_icon, - label: themePrefix("composer_title"), - action: (toolbarEvent) => { - modal.show(InsertJitsi, { model: { toolbarEvent } }); - }, - }); - } + api.addComposerToolbarPopupMenuOption({ + icon: settings.button_icon, + label: themePrefix("composer_title"), + action: (toolbarEvent) => { + modal.show(InsertJitsi, { + model: { insertMeeting: toolbarEvent.addText }, + }); + }, + }); } else { - if ( - settings.only_available_to_staff && - currentUser && - !currentUser.staff - ) { - return; - } api.onToolbarCreate((toolbar) => { toolbar.addButton({ title: themePrefix("composer_title"), @@ -101,12 +133,12 @@ export default { group: "insertions", icon: settings.button_icon, perform: (toolbarEvent) => - modal.show(InsertJitsi, { model: { toolbarEvent } }), + modal.show(InsertJitsi, { + model: { insertMeeting: toolbarEvent.addText }, + }), }); }); } - - api.decorateCooked(attachJitsi, { id: "discourse-jitsi" }); }); }, }; diff --git a/settings.yml b/settings.yml index 4caeb2e..8cd4b53 100644 --- a/settings.yml +++ b/settings.yml @@ -12,6 +12,16 @@ jitsi_script_src: button_icon: default: "video" description: "Enter the name of the FontAwesome 5 icon to display in the Jitsi button. " +chat_button: + default: true + description: "Integrate with Discourse Chat" +chat_button_position: + description: "Position of the button within the chat window" + default: dropdown + type: enum + choices: + - dropdown + - inline svg_icons: default: "video" type: "list" From 647159501edc0ac46878f040f25ffd0c78930d5b Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Tue, 23 Jan 2024 20:54:29 +1300 Subject: [PATCH 04/20] FEATURE: header button integration --- about.json | 2 +- common/common.scss | 4 + .../components/modal/insert-jitsi.gjs | 147 ++++++++++++++++-- .../discourse/initializers/insert-jitsi.js | 47 +++++- locales/en.yml | 5 + settings.yml | 6 + 6 files changed, 191 insertions(+), 20 deletions(-) diff --git a/about.json b/about.json index 4c01f75..5a031be 100644 --- a/about.json +++ b/about.json @@ -3,6 +3,6 @@ "component": true, "modifiers": { "csp_extensions": ["script_src: https://meet.jit.si/external_api.js"], - "svg_icons": ["video"] + "svg_icons": ["video", "copy"] } } diff --git a/common/common.scss b/common/common.scss index 442f3ce..782e726 100644 --- a/common/common.scss +++ b/common/common.scss @@ -5,4 +5,8 @@ color: $primary-medium; max-width: 260px; } + + .desc--bottom { + margin-top: 0rem; + } } diff --git a/javascripts/discourse/components/modal/insert-jitsi.gjs b/javascripts/discourse/components/modal/insert-jitsi.gjs index 58ecd2d..eb72b9d 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.gjs +++ b/javascripts/discourse/components/modal/insert-jitsi.gjs @@ -9,11 +9,92 @@ import DModal from "discourse/components/d-modal"; import TextField from "discourse/components/text-field"; import i18n from "discourse-common/helpers/i18n"; +function purifyRoomID(roomID) { + let purifiedRoomID = roomID.trim(); + + // Strip non-alphanumeric characters for better URL safety and encoding + purifiedRoomID = purifiedRoomID.replace(/[^a-zA-Z0-9 ]/g, ""); + + // Collapse spaces into camel case for better URL encoding + purifiedRoomID = purifiedRoomID + .replace(/\w+/g, function (txt) { + // uppercase first letter and add rest unchanged + return txt.charAt(0).toUpperCase() + txt.substring(1); + }) + // remove any spaces + .replace(/\s/g, ""); + return purifiedRoomID; +} + +function getRandomID() { + return Math.random().toString(36).slice(-8); +} + +function fallbackCopyText(text) { + const textArea = document.createElement("textarea"); + textArea.value = text; + + // Avoid scrolling to bottom + textArea.style.position = "fixed"; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand("copy"); + } catch (err) { + console.error("Unable to copy text", err); + + // Final fallback using a prompt + prompt("Copy to clipboard: Ctrl+C, Enter", text); + } finally { + document.body.removeChild(textArea); + } +} + +function copyText(text) { + // Fallback for outdated browsers + if (!navigator.clipboard) { + fallbackCopyText(text); + return; + } + + navigator.clipboard.writeText(text).catch((err) => { + console.error("Failed to copy to clipboard", err); + fallbackCopyText(text); + }); +} + export default class InsertJitsi extends Component { @tracked mobileIframe = settings.default_mobile_iframe; @tracked desktopIframe = settings.default_desktop_iframe; @tracked jitsiRoom = ""; @tracked buttonText = ""; + // Randomly generated meeting ID + randomRoomID = getRandomID(); + + get roomID() { + let roomID = ""; + + // User specified room ID + if (this.jitsiRoom.length > 0) { + roomID = purifyRoomID(this.jitsiRoom); + } + + // If a custom room ID is empty or not specified use a random one + if (roomID.length === 0) { + roomID = this.randomRoomID; + } + + return roomID; + } + + get roomURL() { + const domain = settings.meet_jitsi_domain; + const roomURL = new URL(this.roomID, `https://${domain}`); + return roomURL.toString(); + } keyDown(e) { if (e.keyCode === 13) { @@ -23,21 +104,15 @@ export default class InsertJitsi extends Component { } } - randomID() { - return Math.random().toString(36).slice(-8); - } - @action insert() { - const btnTxt = this.buttonText ? ` label="${this.buttonText}"` : ""; - const roomID = this.jitsiRoom || this.randomID(); - let text; if (this.args.model.plainText) { - const domain = settings.meet_jitsi_domain; - text = `https://${domain}/${roomID}`; + text = this.roomURL; } else { + const roomID = this.roomId; + const btnTxt = this.buttonText ? ` label="${this.buttonText}"` : ""; text = `[wrap=discourse-jitsi room="${roomID}"${btnTxt} mobileIframe="${this.mobileIframe}" desktopIframe="${this.desktopIframe}"][/wrap]`; } @@ -45,6 +120,17 @@ export default class InsertJitsi extends Component { this.args.closeModal(); } + @action + openRoomURL() { + this.args.closeModal(); + window.open(this.roomURL, "_blank"); + } + + @action + copyRoomURL() { + copyText(this.roomURL); + } + @action cancel() { this.args.closeModal(); @@ -66,6 +152,7 @@ export default class InsertJitsi extends Component { @value={{this.jitsiRoom}} @autofocus="autofocus" @autocomplete="off" + minlength="6" />
{{i18n (themePrefix "modal.room_field_description") @@ -99,15 +186,45 @@ export default class InsertJitsi extends Component {
{{/unless}} + + {{#if @model.createOnly}} +
+ +
+ + +
+

+ {{i18n (themePrefix "modal.url_description")}} +

+
+ {{/if}} <:footer> - + {{#if @model.createOnly}} + + {{else}} + + {{/if}} diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index 98d6041..17df2ee 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -2,7 +2,7 @@ /* global JitsiMeetExternalAPI */ import loadScript from "discourse/lib/load-script"; import { withPluginApi } from "discourse/lib/plugin-api"; -import { iconHTML } from "discourse-common/lib/icon-library"; +import { iconHTML, iconNode } from "discourse-common/lib/icon-library"; import I18n from "I18n"; import InsertJitsi from "../components/modal/insert-jitsi"; @@ -80,10 +80,45 @@ export default { api.decorateCooked(attachJitsi, { id: "discourse-jitsi" }); // Ensure the current user has access to the feature - if (settings.only_available_to_staff && currentUser && !currentUser.staff) { + if ( + settings.only_available_to_staff && + currentUser && + !currentUser.staff + ) { return; } + // Header button + if (settings.header_button) { + // Decorate the header icons section + api.decorateWidget("header-icons:before", (helper) => { + // Create the new header icon + return helper.h("li.header-icon-meeting", [ + helper.h( + "a.icon.btn-flat", + { + onclick: () => { + modal.show(InsertJitsi, { + model: { + // This model cannot insert + insertMeeting: (_) => {}, + createOnly: true, + plainText: true, + }, + }); + }, + attributes: { + target: "_blank", + title: themePrefix("start_title"), + }, + }, + // Use the video icon + iconNode(settings.button_icon) + ), + ]); + }); + } + // Chat plugin integration if (settings.chat_button && api.registerChatComposerButton) { const chat = api.container.lookup("service:chat"); @@ -121,7 +156,9 @@ export default { label: themePrefix("composer_title"), action: (toolbarEvent) => { modal.show(InsertJitsi, { - model: { insertMeeting: toolbarEvent.addText }, + model: { + insertMeeting: toolbarEvent.addText, + }, }); }, }); @@ -134,7 +171,9 @@ export default { icon: settings.button_icon, perform: (toolbarEvent) => modal.show(InsertJitsi, { - model: { insertMeeting: toolbarEvent.addText }, + model: { + insertMeeting: toolbarEvent.addText, + }, }), }); }); diff --git a/locales/en.yml b/locales/en.yml index 1ce91b2..ba6b50a 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,12 +1,17 @@ en: composer_title: Discourse Jitsi room_label: Jitsi room ID + room_url_label: Room URL button_text_label: Button label launch_jitsi: Start Video Conference modal: room_field_description: Enter an ID for your room (or leave empty to use a randomly generated ID). + url_description: You can connect to and share the room using the URL below insert: Insert cancel: Cancel title: Add Jitsi Integration + copy: Copy Room URL + title_create: Launch Jitsi Integration mobile_iframe: Show in an iframe on mobile devices desktop_iframe: Show in an iframe on desktop devices + launch: Start Video Conference diff --git a/settings.yml b/settings.yml index 9ff4b5a..bec1a69 100644 --- a/settings.yml +++ b/settings.yml @@ -12,6 +12,12 @@ jitsi_script_src: button_icon: default: "video" description: "Enter the name of the FontAwesome 5 icon to display in the Jitsi button. " +copy_button_icon: + default: "copy" + description: "Enter the name of the FontAwesome 5 icon to display as the copy button. " +header_button: + default: false + description: "Display a video button in the header for creating a meeting" chat_button: default: true description: "Integrate with Discourse Chat" From 39dc347505cdff7a859491d398b99535c02965b7 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 24 Jan 2024 07:43:49 +1300 Subject: [PATCH 05/20] FIX: incorrect action name for copy room URL --- javascripts/discourse/components/modal/insert-jitsi.gjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascripts/discourse/components/modal/insert-jitsi.gjs b/javascripts/discourse/components/modal/insert-jitsi.gjs index eb72b9d..aa52019 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.gjs +++ b/javascripts/discourse/components/modal/insert-jitsi.gjs @@ -198,7 +198,7 @@ export default class InsertJitsi extends Component { class="btn-primary btn-icon btn-copy" @disabled={{this.insertDisabled}} @title={{themePrefix "modal.copy"}} - @action={{this.copyRoomURL1}} + @action={{this.copyRoomURL}} @icon={{settings.copy_button_icon}} /> From 27b666d35a5944d17d00ae1af09cdc7dddd04c64 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 24 Jan 2024 10:42:39 +1300 Subject: [PATCH 06/20] FIX: room ID undefined --- .../components/modal/insert-jitsi.gjs | 2 +- .../components/modal/insert-jitsi.hbs | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 javascripts/discourse/components/modal/insert-jitsi.hbs diff --git a/javascripts/discourse/components/modal/insert-jitsi.gjs b/javascripts/discourse/components/modal/insert-jitsi.gjs index aa52019..bac2d75 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.gjs +++ b/javascripts/discourse/components/modal/insert-jitsi.gjs @@ -111,7 +111,7 @@ export default class InsertJitsi extends Component { if (this.args.model.plainText) { text = this.roomURL; } else { - const roomID = this.roomId; + const roomID = this.roomID; const btnTxt = this.buttonText ? ` label="${this.buttonText}"` : ""; text = `[wrap=discourse-jitsi room="${roomID}"${btnTxt} mobileIframe="${this.mobileIframe}" desktopIframe="${this.desktopIframe}"][/wrap]`; } diff --git a/javascripts/discourse/components/modal/insert-jitsi.hbs b/javascripts/discourse/components/modal/insert-jitsi.hbs new file mode 100644 index 0000000..8fa0557 --- /dev/null +++ b/javascripts/discourse/components/modal/insert-jitsi.hbs @@ -0,0 +1,44 @@ + + <:body> +
+
+ + +
{{i18n + (themePrefix "modal.room_field_description") + }}
+
+ + {{#unless (or @model.plainText settings.hide_iframe_buttons)}} + +
+ + +
+ +
+ +
+ +
+ +
+ + {{/unless}} +
+ + <:footer> + + +
\ No newline at end of file From e8c47e350e001784549bff28808496a2d42c2fb7 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 24 Jan 2024 11:07:28 +1300 Subject: [PATCH 07/20] FEATURE: chat integration and usability improvements Added integration with Discourse Chat to allow creating a link within the options of a chat window Open plain meeting links in a new browser tab rather than overwriting the current tab Added settings for the new chat button integration Separated template from javascript for better maintainability Added new settings for choose the default options in regards to iframes --- .eslintrc.cjs | 2 +- .../components/modal/insert-jitsi.gjs | 103 ------------------ .../components/modal/insert-jitsi.hbs | 58 ++++++++++ .../components/modal/insert-jitsi.js | 47 ++++++++ .../discourse/initializers/insert-jitsi.js | 78 +++++++++---- settings.yml | 19 ++++ 6 files changed, 182 insertions(+), 125 deletions(-) delete mode 100644 javascripts/discourse/components/modal/insert-jitsi.gjs create mode 100644 javascripts/discourse/components/modal/insert-jitsi.hbs create mode 100644 javascripts/discourse/components/modal/insert-jitsi.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs index ee89c90..be1a9f3 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1 +1 @@ -module.exports = require("@discourse/lint-configs/eslint"); \ No newline at end of file +module.exports = require("@discourse/lint-configs/eslint"); diff --git a/javascripts/discourse/components/modal/insert-jitsi.gjs b/javascripts/discourse/components/modal/insert-jitsi.gjs deleted file mode 100644 index 7aef283..0000000 --- a/javascripts/discourse/components/modal/insert-jitsi.gjs +++ /dev/null @@ -1,103 +0,0 @@ -/*eslint no-undef:0 */ - -import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { Input } from "@ember/component"; -import { action } from "@ember/object"; -import DButton from "discourse/components/d-button"; -import DModal from "discourse/components/d-modal"; -import TextField from "discourse/components/text-field"; -import i18n from "discourse-common/helpers/i18n"; - -export default class InsertJitsi extends Component { - @tracked mobileIframe = true; - @tracked desktopIframe = true; - @tracked jitsiRoom = ""; - @tracked buttonText = ""; - - keyDown(e) { - if (e.keyCode === 13) { - e.preventDefault(); - e.stopPropagation(); - return false; - } - } - - randomID() { - return Math.random().toString(36).slice(-8); - } - - @action - insert() { - const btnTxt = this.buttonText ? ` label="${this.buttonText}"` : ""; - const roomID = this.jitsiRoom || this.randomID(); - const text = `[wrap=discourse-jitsi room="${roomID}"${btnTxt} mobileIframe="${this.mobileIframe}" desktopIframe="${this.desktopIframe}"][/wrap]`; - this.args.model.toolbarEvent.addText(text); - - this.args.closeModal(); - } - - @action - cancel() { - this.args.closeModal(); - } - - -} diff --git a/javascripts/discourse/components/modal/insert-jitsi.hbs b/javascripts/discourse/components/modal/insert-jitsi.hbs new file mode 100644 index 0000000..a345dc5 --- /dev/null +++ b/javascripts/discourse/components/modal/insert-jitsi.hbs @@ -0,0 +1,58 @@ + + <:body> +
+
+ + +
{{theme-i18n "modal.room_field_description"}}
+
+ + {{#unless @model.plainText}} + +
+ + +
+ + {{#unless (theme-setting "hide_iframe_buttons")}} +
+ +
+ +
+ +
+ {{/unless}} + {{/unless}} +
+ + <:footer> + + +
\ No newline at end of file diff --git a/javascripts/discourse/components/modal/insert-jitsi.js b/javascripts/discourse/components/modal/insert-jitsi.js new file mode 100644 index 0000000..2606447 --- /dev/null +++ b/javascripts/discourse/components/modal/insert-jitsi.js @@ -0,0 +1,47 @@ +/*eslint no-undef:0 */ + +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; + +export default class InsertJitsiComponent extends Component { + @tracked mobileIframe = settings.default_mobile_iframe; + @tracked desktopIframe = settings.default_desktop_iframe; + @tracked jitsiRoom = ""; + @tracked buttonText = ""; + + keyDown(e) { + if (e.keyCode === 13) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + } + + randomID() { + return Math.random().toString(36).slice(-8); + } + + @action + insert() { + const btnTxt = this.buttonText ? ` label="${this.buttonText}"` : ""; + const roomID = this.jitsiRoom || this.randomID(); + + let text; + + if (this.args.model.plainText) { + const domain = settings.meet_jitsi_domain; + text = `https://${domain}/${roomID}`; + } else { + text = `[wrap=discourse-jitsi room="${roomID}"${btnTxt} mobileIframe="${this.mobileIframe}" desktopIframe="${this.desktopIframe}"][/wrap]`; + } + + this.args.model.insertMeeting(text); + this.args.closeModal(); + } + + @action + cancel() { + this.args.closeModal(); + } +} diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index a514607..ec3896c 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -4,7 +4,7 @@ import loadScript from "discourse/lib/load-script"; import { withPluginApi } from "discourse/lib/plugin-api"; import { iconHTML } from "discourse-common/lib/icon-library"; import I18n from "I18n"; -import InsertJitsi from "../components/modal/insert-jitsi"; +import InsertJitsiComponent from "../components/modal/insert-jitsi"; /* eslint-disable */ // prettier-ignore @@ -16,7 +16,7 @@ function launchJitsi($elem, user, site) { (site.mobileView && data.mobileIframe === false) || (!site.mobileView && data.desktopIframe === false) ) { - window.location.href = `https://${domain}/${data.room}`; + window.open(`https://${domain}/${data.room}`, "_blank"); return false; } @@ -66,6 +66,7 @@ function attachJitsi($elem, helper) { }); } } + /* eslint-enable */ export default { @@ -76,24 +77,59 @@ export default { const currentUser = api.getCurrentUser(); const modal = api.container.lookup("service:modal"); + api.decorateCooked(attachJitsi, { id: "discourse-jitsi" }); + + // Ensure the current user has access to the feature + if ( + settings.only_available_to_staff && + currentUser && + !currentUser.staff + ) { + return; + } + + // Chat plugin integration + if (settings.chat_button && api.registerChatComposerButton) { + const chat = api.container.lookup("service:chat"); + + api.registerChatComposerButton({ + title: themePrefix("composer_title"), + id: "insertChatJitsi", + group: "insertions", + position: settings.chat_button_position, + icon: settings.button_icon, + label: themePrefix("composer_title"), + action: () => { + modal.show(InsertJitsiComponent, { + model: { + insertMeeting: (text) => { + // Get the active channel ensuring one is present + const activeChannel = chat.activeChannel; + if (activeChannel === null) { + return; + } + + // Append the meeting link to the draft + activeChannel.draft.message += text; + }, + plainText: true, + }, + }); + }, + }); + } + if (settings.show_in_options_dropdown) { - if (!settings.only_available_to_staff || currentUser?.staff) { - api.addComposerToolbarPopupMenuOption({ - icon: settings.button_icon, - label: themePrefix("composer_title"), - action: (toolbarEvent) => { - modal.show(InsertJitsi, { model: { toolbarEvent } }); - }, - }); - } + api.addComposerToolbarPopupMenuOption({ + icon: settings.button_icon, + label: themePrefix("composer_title"), + action: (toolbarEvent) => { + modal.show(InsertJitsiComponent, { + model: { insertMeeting: toolbarEvent.addText }, + }); + }, + }); } else { - if ( - settings.only_available_to_staff && - currentUser && - !currentUser.staff - ) { - return; - } api.onToolbarCreate((toolbar) => { toolbar.addButton({ title: themePrefix("composer_title"), @@ -101,12 +137,12 @@ export default { group: "insertions", icon: settings.button_icon, perform: (toolbarEvent) => - modal.show(InsertJitsi, { model: { toolbarEvent } }), + modal.show(InsertJitsiComponent, { + model: { insertMeeting: toolbarEvent.addText }, + }), }); }); } - - api.decorateCooked(attachJitsi, { id: "discourse-jitsi" }); }); }, }; diff --git a/settings.yml b/settings.yml index 4caeb2e..8df59ba 100644 --- a/settings.yml +++ b/settings.yml @@ -12,6 +12,25 @@ jitsi_script_src: button_icon: default: "video" description: "Enter the name of the FontAwesome 5 icon to display in the Jitsi button. " +chat_button: + default: true + description: "Integrate with Discourse Chat" +hide_iframe_buttons: + default: false + description: "Hide the choice for iframe settings from users, the default settings below will be used" +default_mobile_iframe: + default: true + description: "Enable iframe mode by default for mobile" +default_desktop_iframe: + default: true + description: "Enable iframe mode by default for desktop" +chat_button_position: + description: "Position of the button within the chat window" + default: dropdown + type: enum + choices: + - dropdown + - inline svg_icons: default: "video" type: "list" From 3ad711d8bcf9d3150496f7010c80c53856c54460 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 24 Jan 2024 11:26:14 +1300 Subject: [PATCH 08/20] FIX: fixed incorrect header component used --- javascripts/discourse/initializers/insert-jitsi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index 2b30592..97b9f0c 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -98,10 +98,10 @@ export default { "a.icon.btn-flat", { onclick: () => { - modal.show(InsertJitsi, { + modal.show(InsertJitsiComponent, { model: { // This model cannot insert - insertMeeting: (_) => {}, + insertMeeting: () => {}, createOnly: true, plainText: true, }, From a03aceb8604504ec5c6017fc7f70d1f5e4c11a51 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 24 Jan 2024 11:30:36 +1300 Subject: [PATCH 09/20] FIX: translations for heading button --- javascripts/discourse/initializers/insert-jitsi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index 97b9f0c..034ef13 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -109,7 +109,7 @@ export default { }, attributes: { target: "_blank", - title: themePrefix("start_title"), + title: I18n.t(themePrefix("start_title")), }, }, // Use the video icon From 2503ec8344ea99af17786292fd0d4416a16a8ee3 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 24 Jan 2024 11:55:00 +1300 Subject: [PATCH 10/20] FEATURE: copy indicator --- common/common.scss | 12 ++++++++++++ .../discourse/components/modal/insert-jitsi.hbs | 4 ++++ .../discourse/components/modal/insert-jitsi.js | 7 +++++++ locales/en.yml | 1 + 4 files changed, 24 insertions(+) diff --git a/common/common.scss b/common/common.scss index 782e726..efc904b 100644 --- a/common/common.scss +++ b/common/common.scss @@ -1,4 +1,6 @@ .insert-jitsi-input { + position: relative; + margin-bottom: 1em; .desc { font-size: $font-down-1; @@ -10,3 +12,13 @@ margin-top: 0rem; } } + +.insert-jitsi-copied { + position: absolute; + top: 0; + right: 0; + background: $primary-high; + padding: 0.5rem; + color: $secondary; + font-size: $font-down-1; +} diff --git a/javascripts/discourse/components/modal/insert-jitsi.hbs b/javascripts/discourse/components/modal/insert-jitsi.hbs index f99907e..d817449 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.hbs +++ b/javascripts/discourse/components/modal/insert-jitsi.hbs @@ -53,6 +53,10 @@ {{theme-i18n "room_url_label"}}
+ {{#if this.copied}} + {{theme-i18n "modal.copied"}} + {{/if}} + { + this.copied = false; + }, 500); } @action diff --git a/locales/en.yml b/locales/en.yml index ba6b50a..2a7394f 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -11,6 +11,7 @@ en: cancel: Cancel title: Add Jitsi Integration copy: Copy Room URL + copied: Copied title_create: Launch Jitsi Integration mobile_iframe: Show in an iframe on mobile devices desktop_iframe: Show in an iframe on desktop devices From b55465bcb8d0d6b7d9d80e342ff7a29224a13be0 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 24 Jan 2024 12:34:57 +1300 Subject: [PATCH 11/20] DEV: increase copied text display time --- javascripts/discourse/components/modal/insert-jitsi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascripts/discourse/components/modal/insert-jitsi.js b/javascripts/discourse/components/modal/insert-jitsi.js index 9f45402..cc18755 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.js +++ b/javascripts/discourse/components/modal/insert-jitsi.js @@ -133,7 +133,7 @@ export default class InsertJitsiComponent extends Component { setTimeout(() => { this.copied = false; - }, 500); + }, 1000); } @action From 246b31d914c943a9970d3325c4c1d1a5170e0085 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Wed, 24 Jan 2024 14:51:15 +1300 Subject: [PATCH 12/20] FEATURE: condensed button location selection --- .../components/modal/insert-jitsi.hbs | 4 +- .../discourse/initializers/insert-jitsi.js | 84 ++++++++++++------- settings.yml | 21 ++--- 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/javascripts/discourse/components/modal/insert-jitsi.hbs b/javascripts/discourse/components/modal/insert-jitsi.hbs index d817449..0ec4cdc 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.hbs +++ b/javascripts/discourse/components/modal/insert-jitsi.hbs @@ -54,7 +54,9 @@
{{#if this.copied}} - {{theme-i18n "modal.copied"}} + + {{theme-i18n "modal.copied"}} + {{/if}} diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index 034ef13..c00f172 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -67,6 +67,34 @@ function attachJitsi($elem, helper) { } } +function createChatButton(chat, id, position) { + return { + title: themePrefix("composer_title"), + id, + group: "insertions", + position, + icon: settings.button_icon, + label: themePrefix("composer_title"), + action: () => { + modal.show(InsertJitsiComponent, { + model: { + insertMeeting: (text) => { + // Get the active channel ensuring one is present + const activeChannel = chat.activeChannel; + if (activeChannel === null) { + return; + } + + // Append the meeting link to the draft + activeChannel.draft.message += text; + }, + plainText: true, + }, + }); + }, + }; +} + /* eslint-enable */ export default { @@ -88,8 +116,18 @@ export default { return; } + const locations = settings.locations.split("|").map((value) => + value + // Remove extra spaces + .trim() + // Convert to lowercase + .toLowerCase() + // Replace spaces with a single dash + .replace(/\s/g, "-") + ); + // Header button - if (settings.header_button) { + if (locations.includes("header-icon")) { // Decorate the header icons section api.decorateWidget("header-icons:before", (helper) => { // Create the new header icon @@ -120,37 +158,23 @@ export default { } // Chat plugin integration - if (settings.chat_button && api.registerChatComposerButton) { + if (api.registerChatComposerButton) { const chat = api.container.lookup("service:chat"); - api.registerChatComposerButton({ - title: themePrefix("composer_title"), - id: "insertChatJitsi", - group: "insertions", - position: settings.chat_button_position, - icon: settings.button_icon, - label: themePrefix("composer_title"), - action: () => { - modal.show(InsertJitsiComponent, { - model: { - insertMeeting: (text) => { - // Get the active channel ensuring one is present - const activeChannel = chat.activeChannel; - if (activeChannel === null) { - return; - } - - // Append the meeting link to the draft - activeChannel.draft.message += text; - }, - plainText: true, - }, - }); - }, - }); + if (locations.includes("chat-icon")) { + api.registerChatComposerButton( + createChatButton(chat, "insertChatJitsiInline", "inline") + ); + } + + if (locations.includes("chat-toolbar")) { + api.registerChatComposerButton( + createChatButton(chat, "insertChatJitsi", "dropdown") + ); + } } - if (settings.show_in_options_dropdown) { + if (locations.includes("composer-gear-menu")) { api.addComposerToolbarPopupMenuOption({ icon: settings.button_icon, label: themePrefix("composer_title"), @@ -160,7 +184,9 @@ export default { }); }, }); - } else { + } + + if (locations.includes("composer-toolbar")) { api.onToolbarCreate((toolbar) => { toolbar.addButton({ title: themePrefix("composer_title"), diff --git a/settings.yml b/settings.yml index 697ae4a..8902386 100644 --- a/settings.yml +++ b/settings.yml @@ -1,6 +1,8 @@ -show_in_options_dropdown: - default: false - description: "When unchecked, icon is shown in composer toolbar." +locations: + type: "list" + list_type: "compact" + description: 'The various locations where a video button should be added, when adding manually ensure the names match the following options: "Header Icon","Composer Toolbar","Composer Gear Menu","Chat Icon","Chat Toolbar"' + default: "Header Icon|Composer Toolbar|Composer Gear Menu|Chat Icon|Chat Toolbar" only_available_to_staff: default: false meet_jitsi_domain: @@ -12,16 +14,6 @@ jitsi_script_src: button_icon: default: "video" description: "Enter the name of the FontAwesome 5 icon to display in the Jitsi button. " -chat_button: - default: true - description: "Integrate with Discourse Chat" -chat_button_position: - description: "Position of the button within the chat window" - default: dropdown - type: enum - choices: - - dropdown - - inline hide_iframe_buttons: default: false description: "Hide the choice for iframe settings from users, the default settings below will be used" @@ -31,9 +23,6 @@ default_mobile_iframe: default_desktop_iframe: default: true description: "Enable iframe mode by default for desktop" -header_button: - default: false - description: "Display a video button in the header for creating a meeting" copy_button_icon: default: "copy" description: "Enter the name of the FontAwesome 5 icon to display as the copy button. " From e7af30bb41854b8d15585203784eb2026b738a38 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Thu, 25 Jan 2024 08:30:25 +1300 Subject: [PATCH 13/20] FIX: missing modal for chat button --- javascripts/discourse/initializers/insert-jitsi.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index c00f172..c469388 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -67,7 +67,7 @@ function attachJitsi($elem, helper) { } } -function createChatButton(chat, id, position) { +function createChatButton(chat, modal, id, position) { return { title: themePrefix("composer_title"), id, @@ -163,13 +163,13 @@ export default { if (locations.includes("chat-icon")) { api.registerChatComposerButton( - createChatButton(chat, "insertChatJitsiInline", "inline") + createChatButton(chat, modal, "insertChatJitsiInline", "inline") ); } if (locations.includes("chat-toolbar")) { api.registerChatComposerButton( - createChatButton(chat, "insertChatJitsi", "dropdown") + createChatButton(chat, modal, "insertChatJitsi", "dropdown") ); } } From f7e0aa0fd35a1d6841930a35781692b5a633f9c0 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Thu, 25 Jan 2024 10:44:45 +1300 Subject: [PATCH 14/20] FEATURE: discourse-calendar integration --- .../components/modal/insert-jitsi.js | 69 +--------- .../components/modal/jitsi-event.hbs | 63 +++++++++ .../discourse/components/modal/jitsi-event.js | 120 ++++++++++++++++++ .../initializers/add-jitsi-event-builder.js | 85 +++++++++++++ .../discourse/initializers/insert-jitsi.js | 3 +- javascripts/discourse/lib/jitsi.js | 59 +++++++++ locales/en.yml | 3 + settings.yml | 3 + 8 files changed, 339 insertions(+), 66 deletions(-) create mode 100644 javascripts/discourse/components/modal/jitsi-event.hbs create mode 100644 javascripts/discourse/components/modal/jitsi-event.js create mode 100644 javascripts/discourse/initializers/add-jitsi-event-builder.js create mode 100644 javascripts/discourse/lib/jitsi.js diff --git a/javascripts/discourse/components/modal/insert-jitsi.js b/javascripts/discourse/components/modal/insert-jitsi.js index cc18755..6e943ea 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.js +++ b/javascripts/discourse/components/modal/insert-jitsi.js @@ -1,8 +1,9 @@ -/*eslint no-undef:0 */ +/* global settings */ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; +import { copyText, getRandomID, purifyRoomID } from "../../lib/jitsi"; export default class InsertJitsiComponent extends Component { @tracked mobileIframe = settings.default_mobile_iframe; @@ -12,14 +13,14 @@ export default class InsertJitsiComponent extends Component { @tracked copied = false; // Randomly generated meeting ID - randomRoomID = this.getRandomID(); + randomRoomID = getRandomID(); get roomID() { let roomID = ""; // User specified room ID if (this.jitsiRoom.length > 0) { - roomID = this.purifyRoomID(this.jitsiRoom); + roomID = purifyRoomID(this.jitsiRoom); } // If a custom room ID is empty or not specified use a random one @@ -36,66 +37,6 @@ export default class InsertJitsiComponent extends Component { return roomURL.toString(); } - purifyRoomID(roomID) { - let purifiedRoomID = roomID.trim(); - - // Strip non-alphanumeric characters for better URL safety and encoding - purifiedRoomID = purifiedRoomID.replace(/[^a-zA-Z0-9 ]/g, ""); - - // Collapse spaces into camel case for better URL encoding - purifiedRoomID = purifiedRoomID - .replace(/\w+/g, function (txt) { - // uppercase first letter and add rest unchanged - return txt.charAt(0).toUpperCase() + txt.substring(1); - }) - // remove any spaces - .replace(/\s/g, ""); - return purifiedRoomID; - } - - getRandomID() { - return Math.random().toString(36).slice(-8); - } - - fallbackCopyText(text) { - const textArea = document.createElement("textarea"); - textArea.value = text; - - // Avoid scrolling to bottom - textArea.style.position = "fixed"; - - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - try { - document.execCommand("copy"); - } catch (err) { - // eslint-disable-next-line no-console - console.error("Unable to copy text", err); - - // Final fallback using a prompt - // eslint-disable-next-line no-alert - prompt("Copy to clipboard: Ctrl+C, Enter", text); - } finally { - document.body.removeChild(textArea); - } - } - - copyText(text) { - // Fallback for outdated browsers - if (!navigator.clipboard) { - fallbackCopyText(text); - return; - } - - navigator.clipboard.writeText(text).catch((err) => { - // eslint-disable-next-line no-console - console.error("Failed to copy to clipboard", err); - fallbackCopyText(text); - }); - } - keyDown(e) { if (e.keyCode === 13) { e.preventDefault(); @@ -129,7 +70,7 @@ export default class InsertJitsiComponent extends Component { @action copyRoomURL() { this.copied = true; - this.copyText(this.roomURL); + copyText(this.roomURL); setTimeout(() => { this.copied = false; diff --git a/javascripts/discourse/components/modal/jitsi-event.hbs b/javascripts/discourse/components/modal/jitsi-event.hbs new file mode 100644 index 0000000..4375cdc --- /dev/null +++ b/javascripts/discourse/components/modal/jitsi-event.hbs @@ -0,0 +1,63 @@ + + <:body> + +
+ + + + +
{{theme-i18n "modal.room_field_description"}}
+
+ + + + + + + + + +
+ + <:footer> + + +
\ No newline at end of file diff --git a/javascripts/discourse/components/modal/jitsi-event.js b/javascripts/discourse/components/modal/jitsi-event.js new file mode 100644 index 0000000..ffcd258 --- /dev/null +++ b/javascripts/discourse/components/modal/jitsi-event.js @@ -0,0 +1,120 @@ +/* global settings */ + +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import Group from "discourse/models/group"; +import { getRandomID, purifyRoomID } from "../../lib/jitsi"; + +// Lazy import in case the plugin is not installed +let buildParams; +try { + buildParams = + require("discourse/plugins/discourse-calendar/discourse/lib/raw-event-helper").buildParams; +} catch (e) {} + +export default class JitsiEventComponent extends Component { + @service dialog; + @service siteSettings; + @service store; + + @tracked flash = null; + @tracked startsAt = moment(this.args.model.event.starts_at).tz( + this.args.model.event.timezone || "UTC" + ); + @tracked + endsAt = + this.args.model.event.ends_at && + moment(this.args.model.event.ends_at).tz( + this.args.model.event.timezone || "UTC" + ); + @tracked jitsiRoom = ""; + + // Randomly generated meeting ID + randomRoomID = getRandomID(); + + get roomID() { + let roomID = ""; + + // User specified room ID + if (this.jitsiRoom.length > 0) { + roomID = purifyRoomID(this.jitsiRoom); + } + + // If a custom room ID is empty or not specified use a random one + if (roomID.length === 0) { + roomID = this.randomRoomID; + } + + return roomID; + } + + get roomURL() { + const domain = settings.meet_jitsi_domain; + const roomURL = new URL(this.roomID, `https://${domain}`); + return roomURL.toString(); + } + + get availableRecurrences() { + return [ + { id: "every_day", name: "Every Day" }, + { id: "every_month", name: "Every Month" }, + { id: "every_weekday", name: "Every Weekday" }, + { id: "every_week", name: "Every Week" }, + { id: "every_two_weeks", name: "Every Two Weeks" }, + { id: "every_four_weeks", name: "Every Four Weeks" }, + ]; + } + + @action + groupFinder(term) { + return Group.findAll({ term, ignore_automatic: true }); + } + + @action + onChangeDates(dates) { + this.args.model.onChangeDates(dates); + this.startsAt = dates.from; + this.endsAt = dates.to; + } + + @action + onChangeStatus(newStatus) { + this.args.model.updateEventRawInvitees([]); + this.args.model.updateEventStatus(newStatus); + } + + @action + setRawInvitees(_, newInvitees) { + this.args.model.updateEventRawInvitees(newInvitees); + } + + @action + createEvent() { + if (!this.startsAt) { + this.args.closeModal(); + return; + } + + // Set the event room URL + this.args.model.event.url = this.roomURL; + + const eventParams = buildParams( + this.startsAt, + this.endsAt, + this.args.model.event, + this.siteSettings + ); + const markdownParams = []; + Object.keys(eventParams).forEach((key) => { + let value = eventParams[key]; + markdownParams.push(`${key}="${value}"`); + }); + + this.args.model.toolbarEvent.addText( + `[event ${markdownParams.join(" ")}]\n[/event]` + ); + this.args.closeModal(); + } +} diff --git a/javascripts/discourse/initializers/add-jitsi-event-builder.js b/javascripts/discourse/initializers/add-jitsi-event-builder.js new file mode 100644 index 0000000..2884e9f --- /dev/null +++ b/javascripts/discourse/initializers/add-jitsi-event-builder.js @@ -0,0 +1,85 @@ +/* global themePrefix */ + +import EmberObject from "@ember/object"; +import { withPluginApi } from "discourse/lib/plugin-api"; +import JitsiEventComponent from "../components/modal/jitsi-event"; + +function initializeEventBuilder(api) { + const currentUser = api.getCurrentUser(); + const store = api.container.lookup("service:store"); + const modal = api.container.lookup("service:modal"); + + // Lazy import in case the plugin is not installed + let discoursePostEvent; + try { + discoursePostEvent = require("discourse/plugins/discourse-calendar/discourse/widgets/discourse-post-event"); + } catch (e) { + return; + } + + api.addComposerToolbarPopupMenuOption({ + action: (toolbarEvent) => { + const eventModel = store.createRecord("discourse-post-event-event"); + eventModel.setProperties({ + status: "public", + custom_fields: EmberObject.create({}), + starts_at: moment(), + timezone: moment.tz.guess(), + }); + + modal.show(JitsiEventComponent, { + model: { + event: eventModel, + toolbarEvent, + updateCustomField: (field, value) => + discoursePostEvent.updateCustomField(eventModel, field, value), + updateEventStatus: (status) => + discoursePostEvent.updateEventStatus(eventModel, status), + updateEventRawInvitees: (rawInvitees) => + discoursePostEvent.updateEventRawInvitees(eventModel, rawInvitees), + removeReminder: (reminder) => + discoursePostEvent.removeReminder(eventModel, reminder), + addReminder: () => discoursePostEvent.addReminder(eventModel), + onChangeDates: (changes) => + discoursePostEvent.onChangeDates(eventModel, changes), + updateTimezone: (newTz, startsAt, endsAt) => + discoursePostEvent.updateTimezone( + eventModel, + newTz, + startsAt, + endsAt + ), + }, + }); + }, + group: "insertions", + icon: "calendar-day", + label: themePrefix("create_event"), + condition: (composer) => { + if (!currentUser || !currentUser.can_create_discourse_post_event) { + return false; + } + + const composerModel = composer.model; + return ( + composerModel && + !composerModel.replyingToTopic && + (composerModel.topicFirstPost || + composerModel.creatingPrivateMessage || + (composerModel.editingPost && + composerModel.post && + composerModel.post.post_number === 1)) + ); + }, + }); +} + +export default { + name: "add-jitsi-event-builder", + initialize(container) { + const siteSettings = container.lookup("service:site-settings"); + if (siteSettings.discourse_post_event_enabled) { + withPluginApi("0.8.7", initializeEventBuilder); + } + }, +}; diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index c469388..c9ab182 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -1,5 +1,4 @@ -/*eslint no-undef:0 */ -/* global JitsiMeetExternalAPI */ +/* global JitsiMeetExternalAPI, settings, themePrefix */ import loadScript from "discourse/lib/load-script"; import { withPluginApi } from "discourse/lib/plugin-api"; import { iconHTML, iconNode } from "discourse-common/lib/icon-library"; diff --git a/javascripts/discourse/lib/jitsi.js b/javascripts/discourse/lib/jitsi.js new file mode 100644 index 0000000..8aab679 --- /dev/null +++ b/javascripts/discourse/lib/jitsi.js @@ -0,0 +1,59 @@ +export function getRandomID() { + return Math.random().toString(36).slice(-8); +} + +function fallbackCopyText(text) { + const textArea = document.createElement("textarea"); + textArea.value = text; + + // Avoid scrolling to bottom + textArea.style.position = "fixed"; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand("copy"); + } catch (err) { + // eslint-disable-next-line no-console + console.error("Unable to copy text", err); + + // Final fallback using a prompt + // eslint-disable-next-line no-alert + prompt("Copy to clipboard: Ctrl+C, Enter", text); + } finally { + document.body.removeChild(textArea); + } +} + +export function copyText(text) { + // Fallback for outdated browsers + if (!navigator.clipboard) { + fallbackCopyText(text); + return; + } + + navigator.clipboard.writeText(text).catch((err) => { + // eslint-disable-next-line no-console + console.error("Failed to copy to clipboard", err); + fallbackCopyText(text); + }); +} + +export function purifyRoomID(roomID) { + let purifiedRoomID = roomID.trim(); + + // Strip non-alphanumeric characters for better URL safety and encoding + purifiedRoomID = purifiedRoomID.replace(/[^a-zA-Z0-9 ]/g, ""); + + // Collapse spaces into camel case for better URL encoding + purifiedRoomID = purifiedRoomID + .replace(/\w+/g, function (txt) { + // uppercase first letter and add rest unchanged + return txt.charAt(0).toUpperCase() + txt.substring(1); + }) + // remove any spaces + .replace(/\s/g, ""); + return purifiedRoomID; +} diff --git a/locales/en.yml b/locales/en.yml index 2a7394f..7582b5f 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -4,6 +4,7 @@ en: room_url_label: Room URL button_text_label: Button label launch_jitsi: Start Video Conference + create_event: Create Meeting Event modal: room_field_description: Enter an ID for your room (or leave empty to use a randomly generated ID). url_description: You can connect to and share the room using the URL below @@ -16,3 +17,5 @@ en: mobile_iframe: Show in an iframe on mobile devices desktop_iframe: Show in an iframe on desktop devices launch: Start Video Conference + event_modal: + title: Create Meeting Event diff --git a/settings.yml b/settings.yml index 8902386..f63f9e8 100644 --- a/settings.yml +++ b/settings.yml @@ -5,6 +5,9 @@ locations: default: "Header Icon|Composer Toolbar|Composer Gear Menu|Chat Icon|Chat Toolbar" only_available_to_staff: default: false +discourse_post_event_enabled: + description: "Enable to allow creating meeting events through discourse-calendar integration" + default: false meet_jitsi_domain: default: "meet.jit.si" description: "Domain only (no protocol, no trailing slash)." From a012acbde5ed39d1608fe028e0033446d168d8b3 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Thu, 25 Jan 2024 13:06:42 +1300 Subject: [PATCH 15/20] FIX: header icon title --- javascripts/discourse/initializers/insert-jitsi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index c469388..b7cd5f1 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -147,7 +147,7 @@ export default { }, attributes: { target: "_blank", - title: I18n.t(themePrefix("start_title")), + title: I18n.t(themePrefix("launch_jitsi")), }, }, // Use the video icon From 7914e114afbd3d984caca88c64aa8ddc1a2413f3 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Thu, 25 Jan 2024 16:06:45 +1300 Subject: [PATCH 16/20] DEV: refactor to use new layout, and button replace --- common/common.scss | 57 ++++ .../components/modal/insert-date-time.hbs | 243 ++++++++++++++++++ .../components/modal/insert-date-time.js | 109 ++++++++ .../components/modal/insert-jitsi.js | 2 +- .../components/modal/jitsi-event.hbs | 63 ----- .../discourse/components/modal/jitsi-event.js | 120 --------- .../initializers/add-jitsi-event-builder.js | 85 ------ .../discourse/initializers/insert-jitsi.js | 47 +++- locales/en.yml | 5 +- settings.yml | 15 +- 10 files changed, 463 insertions(+), 283 deletions(-) create mode 100644 javascripts/discourse/components/modal/insert-date-time.hbs create mode 100644 javascripts/discourse/components/modal/insert-date-time.js delete mode 100644 javascripts/discourse/components/modal/jitsi-event.hbs delete mode 100644 javascripts/discourse/components/modal/jitsi-event.js delete mode 100644 javascripts/discourse/initializers/add-jitsi-event-builder.js diff --git a/common/common.scss b/common/common.scss index efc904b..5e24a6f 100644 --- a/common/common.scss +++ b/common/common.scss @@ -22,3 +22,60 @@ color: $secondary; font-size: $font-down-1; } + +.discourse-local-date { + > * { + pointer-events: none; + } + + &.cooked-date { + color: var(--primary); + cursor: pointer; + border-bottom: 1px dashed var(--primary-medium); + white-space: nowrap; + + .d-icon { + color: var(--primary); + } + + &.past { + border-bottom-color: var(--primary-low-mid); + } + + &.past[data-countdown] { + color: var(--primary-medium); + } + } +} + +.locale-dates-previews { + max-width: 250px; + + .preview { + display: flex; + flex-direction: column; + padding: 5px; + margin: 0; + + .timezone { + font-weight: 700; + } + + &.current { + background: var(--tertiary-low); + } + } +} + +.download-calendar { + text-align: right; + cursor: pointer; + margin-top: 0.5em; +} + +// @if (#{$replace_date_time}) { +// .btn.local-dates { +// display: none; +// } +// } + \ No newline at end of file diff --git a/javascripts/discourse/components/modal/insert-date-time.hbs b/javascripts/discourse/components/modal/insert-date-time.hbs new file mode 100644 index 0000000..fd16613 --- /dev/null +++ b/javascripts/discourse/components/modal/insert-date-time.hbs @@ -0,0 +1,243 @@ + + <:body> + +
+ {{#if this.isValid}} + {{#if this.timezoneIsDifferentFromUserTimezone}} +
+ {{i18n "discourse_local_dates.create.form.current_timezone"}} + {{this.formattedCurrentUserTimezone}}{{this.currentPreview}} +
+ {{/if}} + {{else}} +
+ {{i18n "discourse_local_dates.create.form.invalid_date"}} +
+ {{/if}} + + {{this.computeDate}} + +
+
+
+ {{d-icon "calendar-alt"}} + +
+ +
+ {{d-icon "calendar-alt"}} + + {{#if this.toFilled}} + + {{/if}} +
+ + {{#unless this.site.mobileView}} + + {{/unless}} +
+ +
+ +
+ + {{#if this.site.mobileView}} + + {{/if}} +
+ + {{#if this.advancedMode}} +
+ {{#unless this.isRange}} +
+ +

{{html-safe + (i18n + "discourse_local_dates.create.form.recurring_description" + ) + }}

+
+ +
+
+ {{/unless}} + +
+ +

{{i18n + "discourse_local_dates.create.form.timezones_description" + }}

+
+ +
+
+ +
+ +

+ {{i18n "discourse_local_dates.create.form.format_description"}} + + {{d-icon "question-circle"}} + +

+
+ +
+
+
+ +
+
+ {{/if}} + + + {{#if this.canCreateEvent}} + + + + + {{#if this.includeJitsi}} + + +
{{theme-i18n + "modal.room_field_description" + }}
+
+ + + + + {{/if}} + {{/if}} +
+
+ + <:footer> + + + + + + +
\ No newline at end of file diff --git a/javascripts/discourse/components/modal/insert-date-time.js b/javascripts/discourse/components/modal/insert-date-time.js new file mode 100644 index 0000000..06ede9d --- /dev/null +++ b/javascripts/discourse/components/modal/insert-date-time.js @@ -0,0 +1,109 @@ +/* global settings */ + +// import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import LocalDatesCreate from "discourse/plugins/discourse-local-dates/discourse/components/modal/local-dates-create"; +import { getRandomID, purifyRoomID } from "../../lib/jitsi"; + +// Lazy import in case the plugin is not installed +let buildParams; +try { + buildParams = + require("discourse/plugins/discourse-calendar/discourse/lib/raw-event-helper").buildParams; +} catch (e) {} + +export default class InsertDateTime extends LocalDatesCreate { + @service dialog; + @service siteSettings; + @service store; + @service composer; + + @tracked jitsiRoom = ""; + @tracked includeJitsi = false; + + // Randomly generated meeting ID + randomRoomID = getRandomID(); + + get roomID() { + let roomID = ""; + + // User specified room ID + if (this.jitsiRoom.length > 0) { + roomID = purifyRoomID(this.jitsiRoom); + } + + // If a custom room ID is empty or not specified use a random one + if (roomID.length === 0) { + roomID = this.randomRoomID; + } + + return roomID; + } + + get canCreateEvent() { + const composerModel = this.composer.model; + return ( + composerModel && + !composerModel.replyingToTopic && + (composerModel.topicFirstPost || + composerModel.creatingPrivateMessage || + (composerModel.editingPost && + composerModel.post && + composerModel.post.post_number === 1)) + ); + } + + get roomURL() { + const domain = settings.meet_jitsi_domain; + const roomURL = new URL(this.roomID, `https://${domain}`); + return roomURL.toString(); + } + + createJitsiEvent() { + const config = this.computedConfig; + + // Set the event room URL + this.model.event.url = this.roomURL; + + if (!config.from) { + this.closeModal(); + return; + } + + const startsAt = config.from.dateTime; + const endsAt = config.to.range ? config.to.dateTime : null; + + this.model.event.setProperties({ + starts_at: startsAt, + ends_at: endsAt, + url: this.roomURL, + }); + + const eventParams = buildParams( + startsAt, + endsAt, + this.model.event, + this.siteSettings + ); + const markdownParams = []; + Object.keys(eventParams).forEach((key) => { + let value = eventParams[key]; + markdownParams.push(`${key}="${value}"`); + }); + + this.model.insertMeeting(`[event ${markdownParams.join(" ")}]\n[/event]`); + + this.closeModal(); + } + + @action + createEvent() { + if (this.includeJitsi) { + this.createJitsiEvent(); + } else { + this.save(); + } + } +} diff --git a/javascripts/discourse/components/modal/insert-jitsi.js b/javascripts/discourse/components/modal/insert-jitsi.js index 6e943ea..47d98e9 100644 --- a/javascripts/discourse/components/modal/insert-jitsi.js +++ b/javascripts/discourse/components/modal/insert-jitsi.js @@ -5,7 +5,7 @@ import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; import { copyText, getRandomID, purifyRoomID } from "../../lib/jitsi"; -export default class InsertJitsiComponent extends Component { +export default class InsertJitsi extends Component { @tracked mobileIframe = settings.default_mobile_iframe; @tracked desktopIframe = settings.default_desktop_iframe; @tracked jitsiRoom = ""; diff --git a/javascripts/discourse/components/modal/jitsi-event.hbs b/javascripts/discourse/components/modal/jitsi-event.hbs deleted file mode 100644 index 4375cdc..0000000 --- a/javascripts/discourse/components/modal/jitsi-event.hbs +++ /dev/null @@ -1,63 +0,0 @@ - - <:body> - -
- - - - -
{{theme-i18n "modal.room_field_description"}}
-
- - - - - - - - - -
- - <:footer> - - -
\ No newline at end of file diff --git a/javascripts/discourse/components/modal/jitsi-event.js b/javascripts/discourse/components/modal/jitsi-event.js deleted file mode 100644 index ffcd258..0000000 --- a/javascripts/discourse/components/modal/jitsi-event.js +++ /dev/null @@ -1,120 +0,0 @@ -/* global settings */ - -import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; -import Group from "discourse/models/group"; -import { getRandomID, purifyRoomID } from "../../lib/jitsi"; - -// Lazy import in case the plugin is not installed -let buildParams; -try { - buildParams = - require("discourse/plugins/discourse-calendar/discourse/lib/raw-event-helper").buildParams; -} catch (e) {} - -export default class JitsiEventComponent extends Component { - @service dialog; - @service siteSettings; - @service store; - - @tracked flash = null; - @tracked startsAt = moment(this.args.model.event.starts_at).tz( - this.args.model.event.timezone || "UTC" - ); - @tracked - endsAt = - this.args.model.event.ends_at && - moment(this.args.model.event.ends_at).tz( - this.args.model.event.timezone || "UTC" - ); - @tracked jitsiRoom = ""; - - // Randomly generated meeting ID - randomRoomID = getRandomID(); - - get roomID() { - let roomID = ""; - - // User specified room ID - if (this.jitsiRoom.length > 0) { - roomID = purifyRoomID(this.jitsiRoom); - } - - // If a custom room ID is empty or not specified use a random one - if (roomID.length === 0) { - roomID = this.randomRoomID; - } - - return roomID; - } - - get roomURL() { - const domain = settings.meet_jitsi_domain; - const roomURL = new URL(this.roomID, `https://${domain}`); - return roomURL.toString(); - } - - get availableRecurrences() { - return [ - { id: "every_day", name: "Every Day" }, - { id: "every_month", name: "Every Month" }, - { id: "every_weekday", name: "Every Weekday" }, - { id: "every_week", name: "Every Week" }, - { id: "every_two_weeks", name: "Every Two Weeks" }, - { id: "every_four_weeks", name: "Every Four Weeks" }, - ]; - } - - @action - groupFinder(term) { - return Group.findAll({ term, ignore_automatic: true }); - } - - @action - onChangeDates(dates) { - this.args.model.onChangeDates(dates); - this.startsAt = dates.from; - this.endsAt = dates.to; - } - - @action - onChangeStatus(newStatus) { - this.args.model.updateEventRawInvitees([]); - this.args.model.updateEventStatus(newStatus); - } - - @action - setRawInvitees(_, newInvitees) { - this.args.model.updateEventRawInvitees(newInvitees); - } - - @action - createEvent() { - if (!this.startsAt) { - this.args.closeModal(); - return; - } - - // Set the event room URL - this.args.model.event.url = this.roomURL; - - const eventParams = buildParams( - this.startsAt, - this.endsAt, - this.args.model.event, - this.siteSettings - ); - const markdownParams = []; - Object.keys(eventParams).forEach((key) => { - let value = eventParams[key]; - markdownParams.push(`${key}="${value}"`); - }); - - this.args.model.toolbarEvent.addText( - `[event ${markdownParams.join(" ")}]\n[/event]` - ); - this.args.closeModal(); - } -} diff --git a/javascripts/discourse/initializers/add-jitsi-event-builder.js b/javascripts/discourse/initializers/add-jitsi-event-builder.js deleted file mode 100644 index 2884e9f..0000000 --- a/javascripts/discourse/initializers/add-jitsi-event-builder.js +++ /dev/null @@ -1,85 +0,0 @@ -/* global themePrefix */ - -import EmberObject from "@ember/object"; -import { withPluginApi } from "discourse/lib/plugin-api"; -import JitsiEventComponent from "../components/modal/jitsi-event"; - -function initializeEventBuilder(api) { - const currentUser = api.getCurrentUser(); - const store = api.container.lookup("service:store"); - const modal = api.container.lookup("service:modal"); - - // Lazy import in case the plugin is not installed - let discoursePostEvent; - try { - discoursePostEvent = require("discourse/plugins/discourse-calendar/discourse/widgets/discourse-post-event"); - } catch (e) { - return; - } - - api.addComposerToolbarPopupMenuOption({ - action: (toolbarEvent) => { - const eventModel = store.createRecord("discourse-post-event-event"); - eventModel.setProperties({ - status: "public", - custom_fields: EmberObject.create({}), - starts_at: moment(), - timezone: moment.tz.guess(), - }); - - modal.show(JitsiEventComponent, { - model: { - event: eventModel, - toolbarEvent, - updateCustomField: (field, value) => - discoursePostEvent.updateCustomField(eventModel, field, value), - updateEventStatus: (status) => - discoursePostEvent.updateEventStatus(eventModel, status), - updateEventRawInvitees: (rawInvitees) => - discoursePostEvent.updateEventRawInvitees(eventModel, rawInvitees), - removeReminder: (reminder) => - discoursePostEvent.removeReminder(eventModel, reminder), - addReminder: () => discoursePostEvent.addReminder(eventModel), - onChangeDates: (changes) => - discoursePostEvent.onChangeDates(eventModel, changes), - updateTimezone: (newTz, startsAt, endsAt) => - discoursePostEvent.updateTimezone( - eventModel, - newTz, - startsAt, - endsAt - ), - }, - }); - }, - group: "insertions", - icon: "calendar-day", - label: themePrefix("create_event"), - condition: (composer) => { - if (!currentUser || !currentUser.can_create_discourse_post_event) { - return false; - } - - const composerModel = composer.model; - return ( - composerModel && - !composerModel.replyingToTopic && - (composerModel.topicFirstPost || - composerModel.creatingPrivateMessage || - (composerModel.editingPost && - composerModel.post && - composerModel.post.post_number === 1)) - ); - }, - }); -} - -export default { - name: "add-jitsi-event-builder", - initialize(container) { - const siteSettings = container.lookup("service:site-settings"); - if (siteSettings.discourse_post_event_enabled) { - withPluginApi("0.8.7", initializeEventBuilder); - } - }, -}; diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index c9ab182..afca4b6 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -1,9 +1,11 @@ /* global JitsiMeetExternalAPI, settings, themePrefix */ +import EmberObject from "@ember/object"; import loadScript from "discourse/lib/load-script"; import { withPluginApi } from "discourse/lib/plugin-api"; import { iconHTML, iconNode } from "discourse-common/lib/icon-library"; import I18n from "I18n"; -import InsertJitsiComponent from "../components/modal/insert-jitsi"; +import InsertDateTime from "../components/modal/insert-date-time"; +import InsertJitsi from "../components/modal/insert-jitsi"; /* eslint-disable */ // prettier-ignore @@ -75,7 +77,7 @@ function createChatButton(chat, modal, id, position) { icon: settings.button_icon, label: themePrefix("composer_title"), action: () => { - modal.show(InsertJitsiComponent, { + modal.show(InsertJitsi, { model: { insertMeeting: (text) => { // Get the active channel ensuring one is present @@ -94,6 +96,24 @@ function createChatButton(chat, modal, id, position) { }; } +function createJitsiEventAction(toolbarEvent, modal, store) { + const eventModel = store.createRecord("discourse-post-event-event"); + eventModel.setProperties({ + status: "public", + custom_fields: EmberObject.create({}), + starts_at: moment(), + timezone: moment.tz.guess(), + }); + + modal.show(InsertDateTime, { + model: { + event: eventModel, + insertMeeting: toolbarEvent.addText, + insertDate: toolbarEvent.addText, + }, + }); +} + /* eslint-enable */ export default { @@ -103,6 +123,7 @@ export default { withPluginApi("0.8.31", (api) => { const currentUser = api.getCurrentUser(); const modal = api.container.lookup("service:modal"); + const store = api.container.lookup("service:store"); api.decorateCooked(attachJitsi, { id: "discourse-jitsi" }); @@ -135,7 +156,7 @@ export default { "a.icon.btn-flat", { onclick: () => { - modal.show(InsertJitsiComponent, { + modal.show(InsertJitsi, { model: { // This model cannot insert insertMeeting: () => {}, @@ -178,7 +199,7 @@ export default { icon: settings.button_icon, label: themePrefix("composer_title"), action: (toolbarEvent) => { - modal.show(InsertJitsiComponent, { + modal.show(InsertJitsi, { model: { insertMeeting: toolbarEvent.addText }, }); }, @@ -193,12 +214,28 @@ export default { group: "insertions", icon: settings.button_icon, perform: (toolbarEvent) => - modal.show(InsertJitsiComponent, { + modal.show(InsertJitsi, { model: { insertMeeting: toolbarEvent.addText }, }), }); }); } + + // Calendar event integration (Replace "Insert date/time") + if (currentUser && currentUser.can_create_discourse_post_event) { + if (settings.replace_date_time) { + api.onToolbarCreate((toolbar) => { + toolbar.addButton({ + title: "discourse_local_dates.title", + id: "insertJitsiEvent", + group: "insertions", + icon: settings.event_button_icon, + perform: (toolbarEvent) => + createJitsiEventAction(toolbarEvent, modal, store), + }); + }); + } + } }); }, }; diff --git a/locales/en.yml b/locales/en.yml index 7582b5f..8c0f147 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -4,7 +4,8 @@ en: room_url_label: Room URL button_text_label: Button label launch_jitsi: Start Video Conference - create_event: Create Meeting Event + include_jitsi_label: Create Jitsi Event + include_jitsi_description: Create a Jitsi event for this date/time modal: room_field_description: Enter an ID for your room (or leave empty to use a randomly generated ID). url_description: You can connect to and share the room using the URL below @@ -17,5 +18,3 @@ en: mobile_iframe: Show in an iframe on mobile devices desktop_iframe: Show in an iframe on desktop devices launch: Start Video Conference - event_modal: - title: Create Meeting Event diff --git a/settings.yml b/settings.yml index f63f9e8..4873852 100644 --- a/settings.yml +++ b/settings.yml @@ -5,18 +5,15 @@ locations: default: "Header Icon|Composer Toolbar|Composer Gear Menu|Chat Icon|Chat Toolbar" only_available_to_staff: default: false -discourse_post_event_enabled: - description: "Enable to allow creating meeting events through discourse-calendar integration" +replace_date_time: default: false + description: 'Replace the "Insert date/time" button with a version that allows creating jitsi events' meet_jitsi_domain: default: "meet.jit.si" description: "Domain only (no protocol, no trailing slash)." jitsi_script_src: default: "https://meet.jit.si/external_api.js" description: "URL of external API javascript file. If you change this, you will also need to add the URL to the 'content security script src' site setting." -button_icon: - default: "video" - description: "Enter the name of the FontAwesome 5 icon to display in the Jitsi button. " hide_iframe_buttons: default: false description: "Hide the choice for iframe settings from users, the default settings below will be used" @@ -26,6 +23,12 @@ default_mobile_iframe: default_desktop_iframe: default: true description: "Enable iframe mode by default for desktop" +button_icon: + default: "video" + description: "Enter the name of the FontAwesome 5 icon to display in the Jitsi button. " +event_button_icon: + default: "calendar-alt" + description: "Enter the name of the FontAwesome 5 icon to display in the Jitsi event button. " copy_button_icon: default: "copy" description: "Enter the name of the FontAwesome 5 icon to display as the copy button. " @@ -33,4 +36,4 @@ svg_icons: default: "video" type: "list" list_type: "compact" - description: "If using a custom icon, add it to this field too (in addition to 'button_icon' above." + description: "If using a custom icon, add it to this field too (in addition to 'button_icon', 'copy_button_icon', and 'event_button_icon' above.)" From a787b51863df175ffd96f3d07c123b38a0b700cc Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Thu, 25 Jan 2024 16:11:08 +1300 Subject: [PATCH 17/20] DEV: inline createJitsiEventAction --- .../discourse/initializers/insert-jitsi.js | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index afca4b6..a98ad2a 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -96,24 +96,6 @@ function createChatButton(chat, modal, id, position) { }; } -function createJitsiEventAction(toolbarEvent, modal, store) { - const eventModel = store.createRecord("discourse-post-event-event"); - eventModel.setProperties({ - status: "public", - custom_fields: EmberObject.create({}), - starts_at: moment(), - timezone: moment.tz.guess(), - }); - - modal.show(InsertDateTime, { - model: { - event: eventModel, - insertMeeting: toolbarEvent.addText, - insertDate: toolbarEvent.addText, - }, - }); -} - /* eslint-enable */ export default { @@ -230,8 +212,23 @@ export default { id: "insertJitsiEvent", group: "insertions", icon: settings.event_button_icon, - perform: (toolbarEvent) => - createJitsiEventAction(toolbarEvent, modal, store), + perform: (toolbarEvent) => { + const eventModel = store.createRecord("discourse-post-event-event"); + eventModel.setProperties({ + status: "public", + custom_fields: EmberObject.create({}), + starts_at: moment(), + timezone: moment.tz.guess(), + }); + + modal.show(InsertDateTime, { + model: { + event: eventModel, + insertMeeting: toolbarEvent.addText, + insertDate: toolbarEvent.addText, + }, + }); + } }); }); } From 040c2fafc165ca2201a0b6f454d03cf5513d6623 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Thu, 25 Jan 2024 19:19:06 +1300 Subject: [PATCH 18/20] FEATURE: support for events, handle recurrence, proper replace --- common/common.scss | 7 - .../components/modal/insert-date-time.hbs | 203 ++++++++++-------- .../components/modal/insert-date-time.js | 41 +++- .../discourse/initializers/insert-jitsi.js | 24 ++- locales/en.yml | 6 +- 5 files changed, 169 insertions(+), 112 deletions(-) diff --git a/common/common.scss b/common/common.scss index 5e24a6f..1bd485a 100644 --- a/common/common.scss +++ b/common/common.scss @@ -72,10 +72,3 @@ cursor: pointer; margin-top: 0.5em; } - -// @if (#{$replace_date_time}) { -// .btn.local-dates { -// display: none; -// } -// } - \ No newline at end of file diff --git a/javascripts/discourse/components/modal/insert-date-time.hbs b/javascripts/discourse/components/modal/insert-date-time.hbs index fd16613..9f90f61 100644 --- a/javascripts/discourse/components/modal/insert-date-time.hbs +++ b/javascripts/discourse/components/modal/insert-date-time.hbs @@ -91,6 +91,57 @@ {{/if}}
+ {{#if this.canCreateEvent}} +
+ +

{{theme-i18n "create_event_description"}}

+
+ +
+
+ + {{#if this.willCreateEvent}} +
+ +

{{theme-i18n "include_jitsi_description"}}

+
+ +
+
+ + {{#if this.includeJitsi}} +
+ +

{{theme-i18n "modal.room_field_description"}}

+
+ +
+
+ {{/if}} + +
+ +

+ {{i18n + "discourse_calendar.discourse_post_event.builder_modal.minimal.checkbox_label" + }} +

+
+ +
+
+ {{/if}} + {{/if}} + {{#if this.advancedMode}}
{{#unless this.isRange}} @@ -117,103 +168,71 @@
{{/unless}} -
- -

{{i18n - "discourse_local_dates.create.form.timezones_description" - }}

-
- -
-
+ {{#unless this.willCreateEvent}} -
- -

- {{i18n "discourse_local_dates.create.form.format_description"}} - - {{d-icon "question-circle"}} - -

-
- +
+ +

{{i18n + "discourse_local_dates.create.form.timezones_description" + }}

+
+ +
-
-
- -
-
- {{/if}} - - {{#if this.canCreateEvent}} - - - - - {{#if this.includeJitsi}} - - -
{{theme-i18n - "modal.room_field_description" - }}
-
- - -
{{/if}} diff --git a/javascripts/discourse/components/modal/insert-date-time.js b/javascripts/discourse/components/modal/insert-date-time.js index 06ede9d..24eb669 100644 --- a/javascripts/discourse/components/modal/insert-date-time.js +++ b/javascripts/discourse/components/modal/insert-date-time.js @@ -6,7 +6,6 @@ import { action } from "@ember/object"; import { inject as service } from "@ember/service"; import LocalDatesCreate from "discourse/plugins/discourse-local-dates/discourse/components/modal/local-dates-create"; import { getRandomID, purifyRoomID } from "../../lib/jitsi"; - // Lazy import in case the plugin is not installed let buildParams; try { @@ -22,6 +21,7 @@ export default class InsertDateTime extends LocalDatesCreate { @tracked jitsiRoom = ""; @tracked includeJitsi = false; + @tracked willCreateEvent = false; // Randomly generated meeting ID randomRoomID = getRandomID(); @@ -61,11 +61,23 @@ export default class InsertDateTime extends LocalDatesCreate { return roomURL.toString(); } - createJitsiEvent() { - const config = this.computedConfig; + get recurringOptions() { + if (this.willCreateEvent) { + return [ + { id: "every_day", name: "Every Day" }, + { id: "every_month", name: "Every Month" }, + { id: "every_weekday", name: "Every Weekday" }, + { id: "every_week", name: "Every Week" }, + { id: "every_two_weeks", name: "Every Two Weeks" }, + { id: "every_four_weeks", name: "Every Four Weeks" }, + ]; + } else { + return super.recurringOptions; + } + } - // Set the event room URL - this.model.event.url = this.roomURL; + saveEvent() { + const config = this.computedConfig; if (!config.from) { this.closeModal(); @@ -74,11 +86,24 @@ export default class InsertDateTime extends LocalDatesCreate { const startsAt = config.from.dateTime; const endsAt = config.to.range ? config.to.dateTime : null; + const url = this.includeJitsi ? this.roomURL : null; + + // Ensure recurrence exists within current type + let recurrence = null; + let recurringOptions = this.recurringOptions; + if ( + recurringOptions.find((option) => option.id === this.recurring) !== + undefined + ) { + recurrence = this.recurring; + } this.model.event.setProperties({ starts_at: startsAt, ends_at: endsAt, - url: this.roomURL, + url, + recurrence, + timezone: this.timezone, }); const eventParams = buildParams( @@ -100,8 +125,8 @@ export default class InsertDateTime extends LocalDatesCreate { @action createEvent() { - if (this.includeJitsi) { - this.createJitsiEvent(); + if (this.willCreateEvent) { + this.saveEvent(); } else { this.save(); } diff --git a/javascripts/discourse/initializers/insert-jitsi.js b/javascripts/discourse/initializers/insert-jitsi.js index a98ad2a..91635a0 100644 --- a/javascripts/discourse/initializers/insert-jitsi.js +++ b/javascripts/discourse/initializers/insert-jitsi.js @@ -98,6 +98,18 @@ function createChatButton(chat, modal, id, position) { /* eslint-enable */ +function removeToolbarItem(toolbar, group, id) { + const targetGroup = toolbar.groups.find((value) => value.group === group); + if (targetGroup !== undefined) { + const buttonIndex = targetGroup.buttons.findIndex( + (button) => button.id === id + ); + if (buttonIndex !== -1) { + targetGroup.buttons.splice(buttonIndex, 1); + } + } +} + export default { name: "insert-jitsi", @@ -149,7 +161,7 @@ export default { }, attributes: { target: "_blank", - title: I18n.t(themePrefix("start_title")), + title: I18n.t(themePrefix("launch_jitsi")), }, }, // Use the video icon @@ -207,13 +219,19 @@ export default { if (currentUser && currentUser.can_create_discourse_post_event) { if (settings.replace_date_time) { api.onToolbarCreate((toolbar) => { + // Remove the existing toolbar item + removeToolbarItem(toolbar, "extras", "local-dates"); + + // Add our new toolbar item toolbar.addButton({ title: "discourse_local_dates.title", id: "insertJitsiEvent", group: "insertions", icon: settings.event_button_icon, perform: (toolbarEvent) => { - const eventModel = store.createRecord("discourse-post-event-event"); + const eventModel = store.createRecord( + "discourse-post-event-event" + ); eventModel.setProperties({ status: "public", custom_fields: EmberObject.create({}), @@ -228,7 +246,7 @@ export default { insertDate: toolbarEvent.addText, }, }); - } + }, }); }); } diff --git a/locales/en.yml b/locales/en.yml index 8c0f147..e0f431e 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -4,8 +4,10 @@ en: room_url_label: Room URL button_text_label: Button label launch_jitsi: Start Video Conference - include_jitsi_label: Create Jitsi Event - include_jitsi_description: Create a Jitsi event for this date/time + create_event_label: Create an event + create_event_description: Create an event for this date/time + include_jitsi_label: Include Jitsi URL + include_jitsi_description: Include a Jitsi room URL with this event modal: room_field_description: Enter an ID for your room (or leave empty to use a randomly generated ID). url_description: You can connect to and share the room using the URL below From cb0d4812e0f6ceabd0bacdaa6f8988ba512e7182 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Sat, 27 Jan 2024 11:26:39 +1300 Subject: [PATCH 19/20] FIX: hide video icon on smaller screens --- common/common.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/common.scss b/common/common.scss index 1bd485a..f1cd631 100644 --- a/common/common.scss +++ b/common/common.scss @@ -72,3 +72,10 @@ cursor: pointer; margin-top: 0.5em; } + + +@media screen and (max-width: 350px) { + .header-icon-meeting, .chat-composer-button.-insertChatJitsiInline { + display: none; + } +} \ No newline at end of file From 39290712b19bcf3da18be1a8af592572b05f977f Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Thu, 29 Feb 2024 10:57:36 +1300 Subject: [PATCH 20/20] DEV: formatted files with prettier --- common/common.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/common.scss b/common/common.scss index f1cd631..dac78de 100644 --- a/common/common.scss +++ b/common/common.scss @@ -73,9 +73,9 @@ margin-top: 0.5em; } - @media screen and (max-width: 350px) { - .header-icon-meeting, .chat-composer-button.-insertChatJitsiInline { + .header-icon-meeting, + .chat-composer-button.-insertChatJitsiInline { display: none; } -} \ No newline at end of file +}