diff --git a/lang/en/rooms.php b/lang/en/rooms.php
index 0e577c348..567d6da3c 100644
--- a/lang/en/rooms.php
+++ b/lang/en/rooms.php
@@ -170,8 +170,10 @@
'invalid_personal_link' => 'This personalised room link is invalid.',
'invitation' => [
'code' => 'Access code',
- 'copied' => 'Copied access information to clipboard',
- 'copy' => 'Copy',
+ 'link_copied' => 'Copied link to clipboard',
+ 'message_copied' => 'Copied invitation message to clipboard',
+ 'copy_link' => 'Copy link',
+ 'copy_invitation_message' => 'Copy invitation message',
'link' => 'Link',
'room' => 'Join ":roomname" with :platform',
'share' => 'Share',
diff --git a/resources/js/components/RoomShareButton.vue b/resources/js/components/RoomShareButton.vue
index cee0741f4..96cdedf30 100644
--- a/resources/js/components/RoomShareButton.vue
+++ b/resources/js/components/RoomShareButton.vue
@@ -17,50 +17,41 @@
{{ $t("rooms.invitation.title") }}
-
-
+
+
-
+
-
-
-
-
-
+
-
-
-
+
+
+
+
-
@@ -89,20 +80,27 @@ const props = defineProps({
});
function copyInvitationText() {
- let message =
+ navigator.clipboard.writeText(invitationMessage.value);
+ toast.success(t("rooms.invitation.message_copied"));
+}
+
+function copyLink() {
+ navigator.clipboard.writeText(roomUrl.value);
+ toast.success(t("rooms.invitation.link_copied"));
+}
+
+const invitationMessage = computed(() => {
+ return (
t("rooms.invitation.room", {
roomname: props.room.name,
platform: settingsStore.getSetting("general.name"),
- }) + "\n";
- message += t("rooms.invitation.link") + ": " + roomUrl.value;
- // If room has access code, include access code in the message
- if (props.room.access_code) {
- message +=
- "\n" + t("rooms.invitation.code") + ": " + formattedAccessCode.value;
- }
- navigator.clipboard.writeText(message);
- toast.success(t("rooms.invitation.copied"));
-}
+ }) +
+ "\n" +
+ t("rooms.invitation.link") +
+ ": " +
+ roomUrl.value
+ );
+});
const roomUrl = computed(() => {
return (
@@ -110,13 +108,10 @@ const roomUrl = computed(() => {
router.resolve({
name: "rooms.view",
params: { id: props.room.id },
+ hash: props.room.access_code
+ ? "#accessCode=" + props.room.access_code
+ : "",
}).href
);
});
-
-const formattedAccessCode = computed(() => {
- return String(props.room.access_code)
- .match(/.{1,3}/g)
- .join("-");
-});
diff --git a/resources/js/components/RoomTabPersonalizedLinksCopyButton.vue b/resources/js/components/RoomTabPersonalizedLinksCopyButton.vue
index 1d9f052ea..01774202b 100644
--- a/resources/js/components/RoomTabPersonalizedLinksCopyButton.vue
+++ b/resources/js/components/RoomTabPersonalizedLinksCopyButton.vue
@@ -52,7 +52,8 @@ function copyLink() {
settingsStore.getSetting("general.base_url") +
router.resolve({
name: "rooms.view",
- params: { id: props.roomId, token: props.token },
+ params: { id: props.roomId },
+ hash: "#token=" + props.token,
}).href;
navigator.clipboard.writeText(link);
toast.info(
diff --git a/resources/js/router.js b/resources/js/router.js
index 9c96e9d5f..c46d1b32b 100644
--- a/resources/js/router.js
+++ b/resources/js/router.js
@@ -134,14 +134,29 @@ export const routes = [
},
{
- path: "/rooms/:id/:token?",
+ path: "/rooms/:id",
name: "rooms.view",
component: RoomView,
meta: { redirectBackAfterLogin: true },
props: (route) => {
return {
id: route.params.id,
- token: route.params.token,
+ };
+ },
+ },
+ {
+ path: "/rooms/:id/:token",
+ redirect: (route) => {
+ // Parse hash parameters
+ const searchParams = new URLSearchParams(route.hash.substring(1));
+
+ // Append or overwrite token parameter
+ searchParams.set("token", route.params.token);
+
+ return {
+ name: "rooms.view",
+ params: { id: route.params.id },
+ hash: "#" + searchParams.toString(),
};
},
},
diff --git a/resources/js/views/RoomsView.vue b/resources/js/views/RoomsView.vue
index 24eb4af41..7e2d407b9 100644
--- a/resources/js/views/RoomsView.vue
+++ b/resources/js/views/RoomsView.vue
@@ -195,7 +195,7 @@
:record-attendance="room.record_attendance"
:record="room.record"
:can-start="room.can_start"
- :token="props.token"
+ :token="token"
:access-code="accessCode"
@invalid-code="handleInvalidCode"
@invalid-token="handleInvalidToken"
@@ -242,24 +242,24 @@ import RoomHeader from "../components/RoomHeader.vue";
import RoomShareButton from "../components/RoomShareButton.vue";
import EventBus from "../services/EventBus.js";
import { EVENT_FORBIDDEN, EVENT_UNAUTHORIZED } from "../constants/events.js";
+import { useUrlSearchParams } from "@vueuse/core";
const props = defineProps({
id: {
type: String,
required: true,
},
- token: {
- type: String,
- default: null,
- },
});
+const hashParams = useUrlSearchParams("hash-params");
+
const reloadInterval = ref(null);
const loading = ref(false); // Room settings/details loading
const room = ref(null); // Room object
const accessCode = ref(null); // Access code to use for requests
const accessCodeInput = ref(""); // Access code input modal
const accessCodeInvalid = ref(null); // Is access code invalid
+const token = ref(null); // Room token used for personalised room access links
const roomLoading = ref(false); // Room loading indicator for initial load
const tokenInvalid = ref(false); // Room token is invalid
const guestsNotAllowed = ref(false); // Access to room was forbidden
@@ -274,11 +274,20 @@ const router = useRouter();
const api = useApi();
onMounted(() => {
- // Prevent authenticated users from using a room token
- if (props.token && authStore.isAuthenticated) {
- toast.info(t("app.flash.guests_only"));
- router.replace({ name: "home" });
- return;
+ if (hashParams.accessCode) {
+ accessCodeInput.value = getHashParamValue("accessCode");
+ hashParams.accessCode = null;
+ login();
+ }
+
+ if (hashParams.token) {
+ // Prevent authenticated users from using a room token
+ if (hashParams.token && authStore.isAuthenticated) {
+ toast.info(t("app.flash.guests_only"));
+ router.replace({ name: "home" });
+ return;
+ }
+ token.value = getHashParamValue("token");
}
EventBus.on(EVENT_FORBIDDEN, reload);
@@ -294,6 +303,41 @@ onUnmounted(() => {
clearInterval(reloadInterval.value);
});
+watch(
+ () => hashParams.accessCode,
+ (value) => {
+ if (!value) return;
+ accessCodeInput.value = getHashParamValue("accessCode");
+ hashParams.accessCode = null;
+ login();
+ },
+);
+
+watch(
+ () => hashParams.token,
+ (value) => {
+ if (!value) return;
+
+ // Prevent authenticated users from using a room token
+ if (hashParams.token && authStore.isAuthenticated) {
+ toast.info(t("app.flash.guests_only"));
+ router.replace({ name: "home" });
+ return;
+ }
+
+ token.value = getHashParamValue("token");
+ clearInterval(reloadInterval.value);
+ tokenInvalid.value = false;
+ load();
+ },
+);
+
+function getHashParamValue(param) {
+ return Array.isArray(hashParams[param])
+ ? hashParams[param][0]
+ : hashParams[param];
+}
+
/**
* Reload room details in a set interval, change in the .env
*/
@@ -362,8 +406,8 @@ function load() {
// Build room api url, include access code if set
const config = {};
- if (props.token) {
- config.headers = { Token: props.token };
+ if (token.value) {
+ config.headers = { Token: token.value };
} else if (accessCode.value != null) {
config.headers = { "Access-Code": accessCode.value };
}
@@ -433,8 +477,8 @@ function reload() {
// Build room api url, include access code if set
const config = {};
- if (props.token) {
- config.headers = { Token: props.token };
+ if (token.value) {
+ config.headers = { Token: token.value };
} else if (accessCode.value != null) {
config.headers = { "Access-Code": accessCode.value };
}
@@ -521,6 +565,7 @@ function setPageTitle(roomName) {
function login() {
// Parse to int
accessCode.value = parseInt(accessCodeInput.value.replace(/[-]/g, ""));
+
// Reload the room with an access code
reload();
}
diff --git a/tests/Frontend/e2e/RoomsViewGeneral.cy.js b/tests/Frontend/e2e/RoomsViewGeneral.cy.js
index 02bbf5833..7b96ac2df 100644
--- a/tests/Frontend/e2e/RoomsViewGeneral.cy.js
+++ b/tests/Frontend/e2e/RoomsViewGeneral.cy.js
@@ -449,24 +449,33 @@ describe("Room View general", function () {
// Check that correct tab is shown
cy.contains("rooms.files.title").should("be.visible");
- // Check if share button is shown correctly
+ // Copy invitation link
cy.get('[data-test="room-share-button"]').click();
cy.get("#invitationLink").should(
"have.value",
- Cypress.config("baseUrl") + "/rooms/abc-def-123",
+ Cypress.config("baseUrl") + "/rooms/abc-def-123#accessCode=508307005",
);
- cy.get("#invitationCode").should("have.value", "508-307-005");
-
- cy.get('[data-test="room-copy-invitation-button"]').click();
- cy.checkToastMessage("rooms.invitation.copied");
+ cy.get('[data-test="room-copy-invitation-link-button"]').click();
+ cy.checkToastMessage("rooms.invitation.link_copied");
+ cy.window().then((win) => {
+ win.navigator.clipboard.readText().then((text) => {
+ expect(text).to.eq(
+ Cypress.config("baseUrl") + "/rooms/abc-def-123#accessCode=508307005",
+ );
+ });
+ });
+ // Copy invitation message
+ cy.get('[data-test="room-share-button"]').click();
+ cy.get('[data-test="room-copy-invitation-message-button"]').click();
+ cy.checkToastMessage("rooms.invitation.message_copied");
cy.window().then((win) => {
win.navigator.clipboard.readText().then((text) => {
expect(text).to.eq(
'rooms.invitation.room_{"roomname":"Meeting One","platform":"PILOS Test"}\nrooms.invitation.link: ' +
Cypress.config("baseUrl") +
- "/rooms/abc-def-123\nrooms.invitation.code: 508-307-005",
+ "/rooms/abc-def-123#accessCode=508307005",
);
});
});
@@ -523,8 +532,10 @@ describe("Room View general", function () {
// Check if share button is shown correctly
cy.get('[data-test="room-share-button"]').click();
- cy.get("#invitationLink").should("include.value", "/rooms/abc-def-123");
- cy.get("#invitationCode").should("have.value", "508-307-005");
+ cy.get("#invitationLink").should(
+ "include.value",
+ "/rooms/abc-def-123#accessCode=508307005",
+ );
});
it("room view as owner", function () {
@@ -568,10 +579,36 @@ describe("Room View general", function () {
// Check that correct tab is shown
cy.contains("rooms.description.title").should("be.visible");
- // Check if share button is shown correctly
+ // Copy invitation link
+ cy.get('[data-test="room-share-button"]').click();
+ cy.get("#invitationLink").should(
+ "have.value",
+ Cypress.config("baseUrl") + "/rooms/abc-def-123#accessCode=508307005",
+ );
+
+ cy.get('[data-test="room-copy-invitation-link-button"]').click();
+ cy.checkToastMessage("rooms.invitation.link_copied");
+ cy.window().then((win) => {
+ win.navigator.clipboard.readText().then((text) => {
+ expect(text).to.eq(
+ Cypress.config("baseUrl") + "/rooms/abc-def-123#accessCode=508307005",
+ );
+ });
+ });
+
+ // Copy invitation message
cy.get('[data-test="room-share-button"]').click();
- cy.get("#invitationLink").should("include.value", "/rooms/abc-def-123");
- cy.get("#invitationCode").should("have.value", "508-307-005");
+ cy.get('[data-test="room-copy-invitation-message-button"]').click();
+ cy.checkToastMessage("rooms.invitation.message_copied");
+ cy.window().then((win) => {
+ win.navigator.clipboard.readText().then((text) => {
+ expect(text).to.eq(
+ 'rooms.invitation.room_{"roomname":"Meeting One","platform":"PILOS Test"}\nrooms.invitation.link: ' +
+ Cypress.config("baseUrl") +
+ "/rooms/abc-def-123#accessCode=508307005",
+ );
+ });
+ });
});
it("room view with token (participant)", function () {
@@ -760,8 +797,10 @@ describe("Room View general", function () {
// Check if share button is shown correctly
cy.get('[data-test="room-share-button"]').click();
- cy.get("#invitationLink").should("include.value", "/rooms/abc-def-123");
- cy.get("#invitationCode").should("have.value", "508-307-005");
+ cy.get("#invitationLink").should(
+ "include.value",
+ "/rooms/abc-def-123#accessCode=508307005",
+ );
});
it("membership button", function () {
diff --git a/tests/Frontend/e2e/RoomsViewPersonalizedLinksTokenActions.cy.js b/tests/Frontend/e2e/RoomsViewPersonalizedLinksTokenActions.cy.js
index c677d4ed2..86c48ea84 100644
--- a/tests/Frontend/e2e/RoomsViewPersonalizedLinksTokenActions.cy.js
+++ b/tests/Frontend/e2e/RoomsViewPersonalizedLinksTokenActions.cy.js
@@ -747,7 +747,7 @@ describe("Rooms view personalized links token actions", function () {
win.navigator.clipboard.readText().then((text) => {
expect(text).to.eq(
Cypress.config("baseUrl") +
- "/rooms/abc-def-123/1ZKctHSaGd7qLDpFa0emXSjoVTkJHkiTm0xajVOXhHU9BA9CCZquf6sDZtAAEGgdO40neF5dXITbH0CxhKM5940eW988WiIKxC8R",
+ "/rooms/abc-def-123#token=1ZKctHSaGd7qLDpFa0emXSjoVTkJHkiTm0xajVOXhHU9BA9CCZquf6sDZtAAEGgdO40neF5dXITbH0CxhKM5940eW988WiIKxC8R",
);
});
});