diff --git a/apps/web/playwright/e2e/voip/element-call.spec.ts b/apps/web/playwright/e2e/voip/element-call.spec.ts index 64f7a6f0f0..da956dfe15 100644 --- a/apps/web/playwright/e2e/voip/element-call.spec.ts +++ b/apps/web/playwright/e2e/voip/element-call.spec.ts @@ -508,15 +508,16 @@ test.describe("Element Call", () => { await openAndJoinCall(page); await app.viewRoomByName("OtherRoom"); - const pipContainer = page.locator(".mx_WidgetPip"); + const pipContainer = page.getByTestId("widget-pip-container"); // We should have a PiP container here. await expect(pipContainer).toBeVisible(); // Leave the call. - const overlay = page.locator(".mx_WidgetPip_overlay"); - await overlay.hover({ timeout: 2000 }); // Show the call footer. - await overlay.getByRole("button", { name: "Leave", exact: true }).click(); + const fakeWidget = page.locator('iframe[title="Element Call"]').contentFrame(); + + // await overlay.hover({ timeout: 2000 }); // Show the call footer. + await fakeWidget.getByRole("button", { name: "Close", exact: true }).click(); // PiP container goes. await expect(pipContainer).not.toBeVisible(); @@ -541,15 +542,14 @@ test.describe("Element Call", () => { await openAndJoinCall(page); await app.viewRoomByName("OtherRoom"); - const pipContainer = page.locator(".mx_WidgetPip"); + const pipContainer = page.getByTestId("widget-pip-container"); // We should have a PiP container here. await expect(pipContainer).toBeVisible(); // Leave the call. - const overlay = page.locator(".mx_WidgetPip_overlay"); - await overlay.hover({ timeout: 2000 }); // Show the call footer. - await overlay.getByRole("button", { name: "Leave", exact: true }).click(); + const fakeWidget = page.locator('iframe[title="Element Call"]').contentFrame(); + await fakeWidget.getByRole("button", { name: "Close", exact: true }).click(); // PiP container goes. await expect(pipContainer).not.toBeVisible(); @@ -578,15 +578,16 @@ test.describe("Element Call", () => { await openAndJoinCall(page, true); await app.viewRoomByName("OtherRoom"); - const pipContainer = page.locator(".mx_WidgetPip"); + const pipContainer = page.getByTestId("widget-pip-container"); // We should have a PiP container here. await expect(pipContainer).toBeVisible(); // Leave the call. - const overlay = page.locator(".mx_WidgetPip_overlay"); - await overlay.hover({ timeout: 2000 }); // Show the call footer. - await overlay.getByRole("button", { name: "Leave", exact: true }).click(); + const fakeWidget = page.locator('iframe[title="Element Call"]').contentFrame(); + + // await overlay.hover({ timeout: 2000 }); // Show the call footer. + await fakeWidget.getByRole("button", { name: "Close", exact: true }).click(); // PiP container goes. await expect(pipContainer).not.toBeVisible(); diff --git a/apps/web/playwright/e2e/widgets/widget-pip-close.spec.ts b/apps/web/playwright/e2e/widgets/widget-pip-close.spec.ts index e8163b5ee2..f2b01f5263 100644 --- a/apps/web/playwright/e2e/widgets/widget-pip-close.spec.ts +++ b/apps/web/playwright/e2e/widgets/widget-pip-close.spec.ts @@ -139,7 +139,7 @@ test.describe("Widget PIP", () => { ); // checks that pip window is opened - await expect(page.locator(".mx_WidgetPip")).toBeVisible(); + await expect(page.getByTestId("widget-pip-container")).toBeVisible(); // checks that widget is opened in pip const iframe = page.frameLocator(`iframe[title="${DEMO_WIDGET_NAME}"]`); @@ -155,7 +155,7 @@ test.describe("Widget PIP", () => { } // checks that pip window is closed - await expect(iframe.locator(".mx_WidgetPip")).not.toBeVisible(); + await expect(iframe.getByTestId("widget-pip-container")).not.toBeVisible(); }); } }); diff --git a/apps/web/res/css/_components.pcss b/apps/web/res/css/_components.pcss index a788baa032..339c4fd37c 100644 --- a/apps/web/res/css/_components.pcss +++ b/apps/web/res/css/_components.pcss @@ -34,7 +34,6 @@ @import "./components/views/location/_ZoomButtons.pcss"; @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; -@import "./components/views/pips/_WidgetPip.pcss"; @import "./components/views/polls/_PollOption.pcss"; @import "./components/views/settings/_AddRemoveThreepids.pcss"; @import "./components/views/settings/devices/_CurrentDeviceSection.pcss"; diff --git a/apps/web/res/css/components/views/pips/_WidgetPip.pcss b/apps/web/res/css/components/views/pips/_WidgetPip.pcss deleted file mode 100644 index b9ad791021..0000000000 --- a/apps/web/res/css/components/views/pips/_WidgetPip.pcss +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -$width: 320px; -$height: 220px; - -.mx_WidgetPip { - width: $width; - height: $height; -} - -.mx_WidgetPip_overlay { - width: $width; - height: $height; - position: absolute; - top: 0; - border-radius: 8px; - overflow: hidden; - color: $call-primary-content; - cursor: pointer; -} - -.mx_WidgetPip_header, -.mx_WidgetPip_footer { - position: absolute; - left: 0; - height: 60px; - width: 100%; - box-sizing: border-box; - transition: opacity ease 0.15s; -} - -.mx_WidgetPip_overlay:not(:hover) { - .mx_WidgetPip_header, - .mx_WidgetPip_footer { - opacity: 0; - } -} - -.mx_WidgetPip_header { - top: 0; - padding: $spacing-12; - display: flex; - font-size: $font-12px; - font-weight: var(--cpd-font-weight-semibold); - background: linear-gradient(rgb(0, 0, 0, 0.9), rgb(0, 0, 0, 0)); -} - -.mx_WidgetPip_backButton { - height: $spacing-24; - display: flex; - align-items: center; - gap: $spacing-12; - - > .mx_Icon { - color: $call-light-quaternary-content; - padding: 0; - } -} - -.mx_WidgetPip_footer { - bottom: 0; - padding: $spacing-12 $spacing-8; - display: flex; - justify-content: flex-end; - align-items: flex-end; - background: linear-gradient(rgb(0, 0, 0, 0), rgb(0, 0, 0, 0.9)); -} diff --git a/apps/web/res/css/views/rooms/_AppsDrawer.pcss b/apps/web/res/css/views/rooms/_AppsDrawer.pcss index 1ee13c4b8d..42b564d9f1 100644 --- a/apps/web/res/css/views/rooms/_AppsDrawer.pcss +++ b/apps/web/res/css/views/rooms/_AppsDrawer.pcss @@ -7,10 +7,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -:root { - --AppTile_mini-height: 220px; -} - .mx_AppsDrawer { --minWidth: 240px; /* TODO this should be 300px but that's too large */ @@ -168,11 +164,11 @@ Please see LICENSE files in the repository root for full details. .mx_AppTile_mini { width: 100%; + height: 100%; margin: 0; padding: 0; display: flex; flex-direction: column; - height: var(--AppTile_mini-height); } .mx_AppTile .mx_AppTile_persistedWrapper, @@ -276,8 +272,8 @@ Please see LICENSE files in the repository root for full details. &.mx_AppTileBody--large, &.mx_AppTileBody--mini { width: 100%; + height: 100%; overflow: hidden; - height: var(--AppTileBody-height); iframe { border: none; @@ -299,10 +295,6 @@ Please see LICENSE files in the repository root for full details. } } - &.mx_AppTileBody--mini { - --AppTileBody-height: var(--AppTile_mini-height); - } - &.mx_AppTileBody--loading { display: flex; flex-direction: column; diff --git a/apps/web/src/components/structures/PictureInPictureDragger.tsx b/apps/web/src/components/structures/PictureInPictureDragger.tsx index 057d600eef..2cadc59a7b 100644 --- a/apps/web/src/components/structures/PictureInPictureDragger.tsx +++ b/apps/web/src/components/structures/PictureInPictureDragger.tsx @@ -18,10 +18,10 @@ const MOVING_AMT = 0.2; const SNAPPING_AMT = 0.1; const PADDING = { - top: 58, - bottom: 58, - left: 76, - right: 8, + top: 80, + bottom: 87, + left: 84, + right: 16, }; /** @@ -53,7 +53,7 @@ export default class PictureInPictureDragger extends React.Component { private initX = 0; private initY = 0; private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH; - private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT; + private desiredTranslationY = PADDING.top; private translationX = this.desiredTranslationX; private translationY = this.desiredTranslationY; private mouseHeld = false; diff --git a/apps/web/src/components/structures/PipContainer.tsx b/apps/web/src/components/structures/PipContainer.tsx index 9dbb6daef7..2eab9efc93 100644 --- a/apps/web/src/components/structures/PipContainer.tsx +++ b/apps/web/src/components/structures/PipContainer.tsx @@ -6,9 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type RefObject, type ReactNode, useRef } from "react"; +import React, { type RefObject, type ReactNode, useRef, useEffect } from "react"; import { CallEvent, CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; +import { useCreateAutoDisposedViewModel, WidgetPipView } from "@element-hq/web-shared-components"; import LegacyCallView from "../views/voip/LegacyCallView"; import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; @@ -21,7 +22,8 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWi import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { SdkContextClass } from "../../contexts/SDKContext"; -import { WidgetPip } from "../views/pips/WidgetPip"; +import RoomAvatar from "../views/avatars/RoomAvatar"; +import { WidgetPipViewModel, type Props as WidgetPipViewModelProps } from "../../viewmodels/room/WidgetPipViewModel"; const SHOW_CALL_IN_STATES = [ CallState.Connected, @@ -46,7 +48,7 @@ interface IState { // they belong to secondaryCall: MatrixCall; - // widget candidate to be displayed in the pip view. + // Widget candidate to be displayed in the PiP view. persistentWidgetId: string | null; persistentRoomId: string | null; showWidgetInPip: boolean; @@ -251,7 +253,7 @@ class PipContainerInner extends React.Component { if (this.state.showWidgetInPip && this.state.persistentWidgetId) { pipContent.push(({ onStartMoving }) => ( - { return ; }; + +type Props = { viewingRoom: boolean } & WidgetPipViewModelProps; + +/** + * A wrapper for the WidgetPipView component. + * + * This exposes the new shared WidgetPipView with the same API as before and how + * it is used in the PipContainerInner component. + * @param props The same props the legacy WidgetPip was using. + * @returns + */ +const WidgetPipWrappedView: React.FC = (props: Props) => { + const vm = useCreateAutoDisposedViewModel(() => new WidgetPipViewModel(props)); + + useEffect(() => { + // Use an effect to update viewingRoom. It is not required in the view but only in the view model. + vm.setViewingRoom(props.viewingRoom); + }, [vm, props.viewingRoom]); + + return ( + } + /> + ); +}; diff --git a/apps/web/src/components/views/pips/WidgetPip.tsx b/apps/web/src/components/views/pips/WidgetPip.tsx deleted file mode 100644 index ed8cdb7e95..0000000000 --- a/apps/web/src/components/views/pips/WidgetPip.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type FC, type RefObject, useCallback, useMemo } from "react"; -import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; -import { ArrowLeftIcon, EndCallIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -import PersistentApp from "../elements/PersistentApp"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { Action } from "../../../dispatcher/actions"; -import { useCallForWidget } from "../../../hooks/useCall"; -import WidgetStore from "../../../stores/WidgetStore"; -import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; -import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; -import Toolbar from "../../../accessibility/Toolbar"; -import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex"; -import { _t } from "../../../languageHandler"; -import { WidgetType } from "../../../widgets/WidgetType"; -import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; -import WidgetUtils from "../../../utils/WidgetUtils"; -import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions"; -import { type ButtonEvent } from "../elements/AccessibleButton"; - -interface Props { - widgetId: string; - room: Room; - viewingRoom: boolean; - onStartMoving: (e: React.MouseEvent) => void; - movePersistedElement: RefObject<(() => void) | null>; -} - -/** - * A picture-in-picture view for a widget. Additional controls are shown if the - * widget is a call of some sort. - */ -export const WidgetPip: FC = ({ widgetId, room, viewingRoom, onStartMoving, movePersistedElement }) => { - const widget = useMemo( - () => WidgetStore.instance.getApps(room.roomId).find((app) => app.id === widgetId)!, - [room, widgetId], - ); - - const roomName = useTypedEventEmitterState( - room, - RoomEvent.Name, - useCallback(() => room.name, [room]), - ); - - const call = useCallForWidget(widgetId, room.roomId); - - const onBackClick = useCallback( - (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - if (call !== null) { - defaultDispatcher.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - view_call: true, - metricsTrigger: "WebFloatingCallWindow", - }); - } else if (viewingRoom) { - WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Center); - } else { - defaultDispatcher.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: "WebFloatingCallWindow", - }); - } - }, - [room, call, widget, viewingRoom], - ); - - const onLeaveClick = useCallback( - (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - if (call !== null) { - call.disconnect().catch((e) => console.error("Failed to leave call", e)); - } else { - // Assumed to be a Jitsi widget - WidgetMessagingStore.instance - .getMessagingForUid(WidgetUtils.getWidgetUid(widget)) - ?.widgetApi?.transport.send(ElementWidgetActions.HangupCall, {}) - .catch((e) => console.error("Failed to leave Jitsi", e)); - } - }, - [call, widget], - ); - - return ( -
- -
- - - - {roomName} - - - {(call !== null || WidgetType.JITSI.matches(widget?.type)) && ( - - - - - - )} -
-
-
- ); -}; diff --git a/apps/web/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/apps/web/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 6c28105060..a5705694e9 100644 --- a/apps/web/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/apps/web/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -76,7 +76,7 @@ const ElementCallSwitch: React.FC = ({ room }) => { return ( { return call; }; -export const useCallForWidget = (widgetId: string, roomId: string): Call | null => { - const call = useCall(roomId); - return call?.widget.id === widgetId ? call : null; -}; - export const useConnectionState = (call: Call | null): ConnectionState => useTypedEventEmitterState( call ?? undefined, diff --git a/apps/web/src/i18n/strings/da.json b/apps/web/src/i18n/strings/da.json index ed6a41b021..9e3b3a4d56 100644 --- a/apps/web/src/i18n/strings/da.json +++ b/apps/web/src/i18n/strings/da.json @@ -72,7 +72,7 @@ "ignore": "Ignorere", "import": "Importér", "invite": "Invitere", - "invite_to_space": "Invitér til gruppe", + "invite_to_space": "Invitér til klynge", "invites_list": "Invitationer", "join": "Deltag", "learn_more": "Få mere at vide", @@ -509,11 +509,11 @@ "privacy": "Privatliv", "private": "Privat", "private_room": "Private rum", - "private_space": "Privat gruppe", + "private_space": "Privat klynge", "profile": "Profil", "public": "Offentlig", "public_room": "Offentlige rum", - "public_space": "Offentlig gruppe", + "public_space": "Offentlig klynge", "qr_code": "QR-kode", "random": "Tilfældig", "reactions": "Reaktioner", @@ -531,8 +531,8 @@ "setup_secure_messages": "Opsæt sikre beskeder", "show_more": "Vis mere", "someone": "Nogen", - "space": "Gruppe", - "spaces": "Grupper", + "space": "Klynge", + "spaces": "Klynger", "sticker": "Klistermærke", "stickerpack": "Klistermærkepakke", "success": "Succes", @@ -547,7 +547,7 @@ "unencrypted": "Ikke krypteret", "unmute": "Slå lyden til", "unnamed_room": "Unavngivet rum", - "unnamed_space": "Unavngivet gruppe", + "unnamed_space": "Unavngivet klynge", "unverified": "Ubekræftet", "updating": "Opdaterer...", "user": "Bruger", @@ -625,7 +625,7 @@ "encrypted_warning": "Du kan ikke deaktivere dette senere. Broer og de fleste bots vil ikke virke endnu.", "encryption_forced": "Din server kræver, at kryptering er aktiveret i private rum.", "encryption_label": "Aktivér end-to-end kryptering", - "error_title": "Gruppen kunne ikke oprettes", + "error_title": "Klyngen kunne ikke oprettes", "generic_error": "Serveren kan være utilgængelig, overbelastet, eller også har du fundet en fejl.", "join_rule_change_notice": "Du kan ændre dette nårsomhelst fra rummets indstillinger", "join_rule_invite": "Privat rum (kun tilgængeligt med invitation)", @@ -633,9 +633,9 @@ "join_rule_knock_label": "Alle kan anmode om at blive medlem, men administratorer eller moderatorer skal give adgang. Du kan ændre dette senere.", "join_rule_public_label": "Enhver vil kunne finde og tilslutte sig dette rum.", "join_rule_public_parent_space_label": "Hvem som helst vil være in stand til at finde og tilslutte sig dette rum, bortset fra medlemmer af ", - "join_rule_restricted": "Synlig for medlemmer af gruppen", + "join_rule_restricted": "Standard", "join_rule_restricted_label": "Alle i vil kunne finde og tilslutte sig dette rum.", - "name_validation_required": "Indtast venligst et navn for gruppen", + "name_validation_required": "Indtast venligst et navn til rummet", "room_visibility_label": "Rummets synlighed", "title_private_room": "Opret et privat rum", "title_public_room": "Opret et offentligt rum", @@ -644,40 +644,40 @@ "unfederated": "Bloker alle, der ikke er en del af %(serverName)s fra nogensinde at deltage i dette rum.", "unfederated_label_default_off": "Du kan vælge at aktivere dette, hvis rummet kun skal bruges til samarbejde med interne teams på din hjemmeserver. Dette kan ikke ændres senere.", "unfederated_label_default_on": "Du kan vælge at deaktivere dette, hvis rummet skal bruges til samarbejde med eksterne teams, der har deres egen hjemmeserver. Dette kan ikke ændres senere.", - "unsupported_version": "Serveren understøtter ikke den oplyste gruppeversion." + "unsupported_version": "Serveren understøtter ikke den oplyste rum-version." }, "create_space": { "add_details_prompt": "Tilføj nogle detaljer, for at hjælpe andre brugere med at genkende den.", "add_details_prompt_2": "Du kan ændre disse nårsomhelst", - "add_existing_rooms_description": "Vælg rum eller samtaler at tilføje, dette er en gruppe kun for dig, ingen får besked om den. Du kan tilføje flere senere.", + "add_existing_rooms_description": "Vælg rum eller samtaler at tilføje, dette er en klynge kun for dig, ingen får besked om den. Du kan tilføje flere senere.", "add_existing_rooms_heading": "Hvad ønsker du at organisere?", "address_label": "Adresse", - "address_placeholder": "f.eks. min-gruppe", + "address_placeholder": "f.eks. min-klynge", "creating": "Opretter...", "creating_rooms": "Opretter rum...", - "done_action": "Gå til min gruppe", + "done_action": "Gå til min klynge", "done_action_first_room": "Gå til min første rum", - "explainer": "Grupper er en ny måde at samle rum og personer på. Hvilken type gruppe vil du gerne lave? Du kan ændre dette senere.", - "failed_create_initial_rooms": "Kunne ikke oprette de første rum i gruppen", - "failed_invite_users": "Det lykkedes ikke at invitere følgende brugere til din gruppe: %(csvUsers)s", + "explainer": "Klynger er en ny måde at samle og finde rum og personer. Hvilken type klynge vil du gerne lave?", + "failed_create_initial_rooms": "Kunne ikke oprette de første rum i klyngen", + "failed_invite_users": "Det lykkedes ikke at invitere følgende brugere til din klynge: %(csvUsers)s", "invite_teammates_by_username": "Invitér med brugernavn", "invite_teammates_description": "Sørg for, at de rette mennesker har adgang. Du kan invitere flere senere.", "invite_teammates_heading": "Invitér dine holdkammerater", "inviting_users": "Inviterer...", - "label": "Opret en gruppe", - "name_required": "Indtast et navn til gruppen", + "label": "Opret en klynge", + "name_required": "Indtast et navn til klyngen", "personal_space": "Bare mig", - "personal_space_description": "En privat gruppe til at organisere dine rum i", + "personal_space_description": "En privat klynge til at organisere dine rum i", "private_description": "Kun for inviterede, bedst til dig selv eller lukkede fællesskaber", - "private_heading": "Din private gruppe", - "private_only_heading": "Din gruppe", + "private_heading": "Din private klynge", + "private_only_heading": "Din klynge", "private_personal_description": "Sørg for at de rette mennesker har adgang til %(name)s", "private_personal_heading": "Hvem arbejder du sammen med?", "private_space": "Mig og mine holdkammerater", - "private_space_description": "En privat gruppe for dig selv og dine holdkammerater", - "public_description": "Åben gruppe for alle, bedst til åbne fællesskaber", - "public_heading": "Din offentlige gruppe", - "search_public_button": "Søg efter offentlige grupper", + "private_space_description": "En privat klynge til dig selv og dine holdkammerater", + "public_description": "Alle kan deltage, bedst til åbne fællesskaber", + "public_heading": "Din offentlige klynge", + "search_public_button": "Søg efter offentlige klynger", "setup_rooms_community_description": "Lad os oprette et rum til hver af dem.", "setup_rooms_community_heading": "Hvad er nogle af de ting, du ønsker at diskutere i %(spaceName)s?", "setup_rooms_description": "Du kan også tilføje flere senere, herunder allerede eksisterende.", @@ -687,13 +687,13 @@ "share_heading": "Del %(name)s", "skip_action": "Spring over for nu", "subspace_adding": "Tilføjer...", - "subspace_beta_notice": "Tilføj en gruppe til en gruppe som du styrer", - "subspace_dropdown_title": "Opret en gruppe", - "subspace_existing_space_prompt": "Ønsker du at tilføje en eksisterende gruppe i stedet?", - "subspace_join_rule_invite_description": "Det er kun inviterede vil være i stand til at se og deltage i denne gruppe.", - "subspace_join_rule_invite_only": "Privat gruppe (kun for inviterede)", - "subspace_join_rule_label": "Gruppens synlighed", - "subspace_join_rule_public_description": "Alle vil kunne finde og deltage i denne gruppe, ikke kun medlemmer af .", + "subspace_beta_notice": "Tilføj en klynge til en klynge som du styrer", + "subspace_dropdown_title": "Opret en klynge", + "subspace_existing_space_prompt": "Ønsker du at tilføje en eksisterende klynge i stedet?", + "subspace_join_rule_invite_description": "Det er kun inviterede, der vil være i stand til at se og deltage i denne klynge.", + "subspace_join_rule_invite_only": "Privat klynge (kun for inviterede)", + "subspace_join_rule_label": "Klyngens synlighed", + "subspace_join_rule_public_description": "Alle vil kunne finde og deltage i denne klynge, ikke kun medlemmer af .", "subspace_join_rule_restricted_description": "Enhver i vil kunne finde og deltage." }, "credits": { @@ -819,7 +819,7 @@ "emoji_picker": { "cancel_search_label": "Annullér søgning" }, - "empty_room": "Tom gruppe", + "empty_room": "Tom klynge", "empty_room_was_name": "Tomt rum (var %(oldName)s)", "encryption": { "access_secret_storage_dialog": { @@ -984,7 +984,7 @@ }, "error_database_closed_title": "%(brand)s holdt op med at fungere", "error_dialog": { - "forget_room_failed": "Kunne ikke glemme gruppen %(errCode)s" + "forget_room_failed": "Kunne ikke glemme rummet %(errCode)s" }, "error_user_not_logged_in": "Brugeren er ikke logget ind", "event_preview": { @@ -1127,10 +1127,10 @@ "url_not_https": "Identitetsserverens URL skal være HTTPS" }, "in_space": "I %(spaceName)s.", - "in_space1_and_space2": "I grupperne %(space1Name)s og %(space2Name)s.", + "in_space1_and_space2": "I klyngerne %(space1Name)s og %(space2Name)s.", "in_space_and_n_other_spaces": { - "one": "I %(spaceName)s og én anden gruppe.", - "other": "I %(spaceName)s og %(count)s andre grupper." + "one": "I %(spaceName)s og én anden klynge.", + "other": "I %(spaceName)s og %(count)s andre klynger." }, "incompatible_browser": { "continue": "Fortsæt alligevel", @@ -1164,31 +1164,31 @@ "email_use_default_is": "Brug en identitetsserver til at invitere via e-mail. Brug standardindstillingen (%(defaultIdentityServerName)s) eller administrer den i Indstillinger.", "email_use_is": "Brug en identitetsserver til at invitere via e-mail. Bestem hvilken under Indstillinger.", "error_already_invited_room": "Brugeren er allerede inviteret til rummet", - "error_already_invited_space": "Brugeren er allerede inviteret til gruppen", + "error_already_invited_space": "Brugeren er allerede inviteret til klyngen", "error_already_joined_room": "Brugeren er allerede i rummet", - "error_already_joined_space": "Brugeren er allerede i gruppen", + "error_already_joined_space": "Brugeren er allerede i klyngen", "error_bad_state": "Brugerens spærring skal ophæves inden de kan blive inviteret.", "error_dm": "Vi kunne ikke oprette din samtale", "error_find_room": "Noget gik galt under forsøget på at invitere brugerne.", "error_find_user_description": "Følgende brugere findes muligvis ikke eller er ugyldige og kan ikke inviteres:%(csvNames)s", "error_find_user_title": "Kunne ikke finde følgende brugere", "error_invite": "Vi kunne ikke invitere disse brugere. Markér venligst de brugere, du vil invitere, og prøv igen.", - "error_permissions_room": "Du har ikke tilladelse til at invitere personer til denne gruppe.", - "error_permissions_space": "Du har ikke tilladelse til at invitere brugere til denne gruppe.", + "error_permissions_room": "Du har ikke tilladelse til at invitere personer til dette rum.", + "error_permissions_space": "Du har ikke tilladelse til at invitere brugere til denne klynge.", "error_profile_undisclosed": "Brugeren findes måske eller måske ikke", "error_transfer_multiple_target": "Et opkald kan kun viderestilles til én bruger.", - "error_unfederated_space": "Denne gruppe er ikke fødereret. Du kan ikke invitere brugere fra eksterne servere.", + "error_unfederated_space": "Denne klynge er ikke fødereret. Du kan ikke invitere brugere fra eksterne servere.", "error_unknown": "Ukendt serverfejl", "error_user_not_found": "Brugeren findes ikke", - "error_version_unsupported_room": "Brugerens homeserver understøtter ikke versionen af denne gruppe.", - "error_version_unsupported_space": "Brugerens hjemmeserver understøtter ikke denne version af gruppen.", + "error_version_unsupported_room": "Brugerens homeserver understøtter ikke versionen af dette rum.", + "error_version_unsupported_space": "Brugerens hjemmeserver understøtter ikke denne version af klyngen.", "failed_generic": "Operation mislykkedes", "failed_title": "Kunne ikke invitere", "invalid_address": "Ukendt adresse", "name_email_mxid_share_room": "Inviter andre ved hjælp af deres navn, e-mailadresse, brugernavn (f.eks. ) eller del dette rum.", - "name_email_mxid_share_space": "Invitér nogen ved at bruge deres navn, e-mail adresse, brugernavn (som eller del denne gruppe", + "name_email_mxid_share_space": "Invitér nogen ved at bruge deres navn, e-mail adresse, brugernavn (som eller del denne klynge", "name_mxid_share_room": "Inviter andre ved at bruge deres navn, brugernavn (f.eks. ) eller del dette rum .", - "name_mxid_share_space": "Invitér nogen ved at bruge deres navn, brugernavn (som eller del denne gruppe", + "name_mxid_share_space": "Invitér nogen ved at bruge deres navn, brugernavn (som eller del denne klynge", "recents_section": "Seneste samtaler", "room_failed_partial": "Vi har sendt til de andre, men nedenstående personer kunne ikke blive inviteret til ", "room_failed_partial_title": "Nogle invitationer kunne ikke blive sendt", @@ -1256,9 +1256,9 @@ "jump_room_search": "Gå til søgning i rum", "jump_to_read_marker": "Hop til den ældste, ulæste besked", "keyboard_shortcuts_tab": "Åbn denne indstillingsfane", - "navigate_next_history": "Næste nyligt besøgte rum eller gruppe", + "navigate_next_history": "Næste nyligt besøgte rum eller klynge", "navigate_next_message_edit": "Gå til næste besked at redigere", - "navigate_prev_history": "Forrige nyligt besøgte rum eller gruppe", + "navigate_prev_history": "Forrige nyligt besøgte rum eller klynge", "navigate_prev_message_edit": "Gå til forrige besked for at redigere", "next_room": "Næste rum eller samtale", "next_unread_room": "Næste ulæste rum eller samtale", @@ -1279,11 +1279,11 @@ "send_sticker": "Send et klistermærke", "shift": "Skift", "space": "Mellemrum", - "switch_to_space": "Skift til gruppe efter nummer", + "switch_to_space": "Skift til klynge efter nummer", "toggle_hidden_events": "Slå synligheden af ​​skjulte begivenheder til/fra", "toggle_microphone_mute": "Slå mikrofonlyd fra", "toggle_right_panel": "Slå højre panel til/fra", - "toggle_space_panel": "Slå panelet for grupper til/fra", + "toggle_space_panel": "Slå panelet for klynger til/fra", "toggle_top_left_menu": "Slå menuen øverst til venstre til/fra", "toggle_webcam_mute": "Slå webcam til/fra", "upload_file": "Upload fil" @@ -1319,7 +1319,7 @@ "group_moderation": "Moderering", "group_profile": "Profil", "group_rooms": "Rum", - "group_spaces": "Grupper", + "group_spaces": "Klynger", "group_themes": "Temaer", "group_threads": "Tråde", "group_ui": "Brugergrænseflade", @@ -1400,7 +1400,7 @@ "leave_room_dialog": { "room_leave_admin_warning": "Du er den eneste administrator i dette rum. Hvis du forlader det, vil ingen være i stand til at ændre dets indstillinger eller foretage andre vigtige handlinger.", "room_leave_mod_warning": "Du er den eneste moderator i dette rum. Hvis du forlader det, vil ingen kunne ændre rummets indstillinger eller foretage andre vigtige handlinger.", - "space_rejoin_warning": "Denne gruppe er ikke offentlig. Du har ikke mulighed for at tilslutte dig uden en invitation." + "space_rejoin_warning": "Denne klynge er ikke offentlig. Du har ikke mulighed for at tilslutte dig uden en invitation." }, "lightbox": { "rotate_left": "Roter mod venstre", @@ -1729,7 +1729,7 @@ "face_pile_tooltip_shortcut_joined": "Inklusive dig, %(commaSeparatedMembers)s", "failed_reject_invite": "Lykkedes ikke med at afvise invitation", "forget_room": "Glem dette rum", - "forget_space": "Glem denne gruppe", + "forget_space": "Glem denne klynge", "header": { "n_people_asking_to_join": { "one": "Beder om at deltage", @@ -1739,10 +1739,10 @@ }, "header_face_pile_tooltip": "Brugere", "header_untrusted_label": "Upålidelig", - "inaccessible": "Rummet eller gruppen er ikke tilgængelig lige nu.", + "inaccessible": "Rummet eller klyngen er ikke tilgængelig lige nu.", "inaccessible_name": "%(roomName)s er ikke tilgængelig på nuværende tidspunkt.", "inaccessible_subtitle_1": "Prøv igen senere, eller bed en administrator af rummet om at tjekke, om du har adgang.", - "inaccessible_subtitle_2": "%(errcode)s blev returneret, mens du forsøgte at få adgang til rummet eller gruppen. Hvis du mener, at denne besked vises ved en fejl, bedes du indsende en fejlrapport.", + "inaccessible_subtitle_2": "%(errcode)s blev returneret, mens du forsøgte at få adgang til rummet eller klyngen. Hvis du mener, at denne besked vises ved en fejl, bedes du indsende en fejlrapport.", "intro": { "dm_caption": "Kun I to deltager i denne samtale, medmindre en af ​​jer inviterer andre til at deltage.", "enable_encryption_prompt": "Aktivér kryptering i indstillingerne.", @@ -1762,7 +1762,7 @@ "invite_sent_to_email": "Denne invitation blev sendt til %(email)s", "invite_sent_to_email_room": "Denne invitation til %(roomName)s blev sendt til %(email)s", "invite_subtitle": "Inviteret af ", - "invite_this_room": "Inviter til denne gruppe", + "invite_this_room": "Inviter til dette rum", "invite_title": "Vil du være med i %(roomName)s?", "inviter_unknown": "Ukendt", "invites_you_text": " inviterer dig", @@ -1773,14 +1773,14 @@ "join_title_account": "Deltag i samtalen med en konto", "joining": "Tilslutter...", "joining_room": "Tilslutter til rummet...", - "joining_space": "Tilslutter til gruppe...", + "joining_space": "Tilslutter til klynge...", "jump_read_marker": "Gå til første ulæste besked.", "jump_to_bottom_button": "Rul ned til de seneste beskeder", "kick_reason": "Årsag: %(reason)s", "kicked_by": "Du blev fjernet af %(memberName)s", "kicked_from_room_by": "Du blev fjernet fra %(roomName)s af %(memberName)s", "knock_cancel_action": "Annuller anmodning", - "knock_denied_subtitle": "Da du er blevet nægtet adgang, kan du ikke tilmelde dig igen, medmindre du bliver inviteret af gruppens administrator eller moderator.", + "knock_denied_subtitle": "Da du er blevet nægtet adgang, kan du ikke tilmelde dig igen, medmindre du bliver inviteret af rummets administrator eller moderator.", "knock_denied_title": "Du er blevet nægtet adgang", "knock_message_field_placeholder": "Besked (valgfrit)", "knock_prompt": "Vil du bede om at deltage?", @@ -1798,7 +1798,7 @@ "no_peek_join_prompt": "%(roomName)s kan ikke forhåndsvises. Ønsker du at tilslutte dig det?", "no_peek_no_name_join_prompt": "Der er ingen forhåndsvisning tilgængelig, vil du tilmelde dig alligevel?", "not_found_subtitle": "Er du sikker på, at du er på det rigtige sted?", - "not_found_title": "Rummet eller gruppen eksisterer ikke.", + "not_found_title": "Rummet eller klyngen eksisterer ikke.", "not_found_title_name": "%(roomName)s findes ikke.", "peek_join_prompt": "Du ser en forhåndsvisning af %(roomName)s. Ønsker du at deltage i det?", "pinned_message_banner": { @@ -1817,8 +1817,8 @@ "one": "Du har %(count)s ulæst notifikation i en tidligere version af dette rum.", "other": "Du har %(count)s ulæste notifikationer i en tidligere version af dette rum." }, - "upgrade_error_description": "Dobbelt-tjek at din server understøtter den valgte gruppeversion og forsøg igen.", - "upgrade_error_title": "Fejl under opgradering af gruppe", + "upgrade_error_description": "Dobbelt-tjek at din server understøtter den valgte rumversion og forsøg igen.", + "upgrade_error_title": "Fejl under opgradering af rum", "upgrade_warning_bar": "Opgradering af dette rum vil lukke den nuværende instans af rummet ned og oprette et opgraderet rum med samme navn.", "upgrade_warning_bar_admins": "Kun rummets administratorer vil se denne advarsel.", "upgrade_warning_bar_unstable": "Dette rum kører version , som denne hjemmeserver har markeret som ustabil.", @@ -1835,11 +1835,11 @@ }, "room_list": { "add_room_label": "Tilføj rum", - "add_space_label": "Tilføj gruppe", + "add_space_label": "Tilføj klynge", "breadcrumbs_empty": "Ingen nyligt besøgte rum", "breadcrumbs_label": "Nyligt besøgte rum", - "failed_add_tag": "Kunne ikke tilføje tag(s): %(tagName)s til gruppen", - "failed_remove_tag": "Kunne ikke fjerne tag(s): %(tagName)s fra gruppen", + "failed_add_tag": "Kunne ikke tilføje tag(s): %(tagName)s til rummet", + "failed_remove_tag": "Kunne ikke fjerne tag(s): %(tagName)s fra rummet", "failed_set_dm_tag": "Kunne ikke indstille tagget til direkte beskeder", "home_menu_label": "Hjemmeindstillinger", "join_public_room_label": "Deltag i offentligt rum", @@ -1875,7 +1875,7 @@ "error_upgrade_description": "Opgraderingen af rummet kunne ikke gennemføres", "error_upgrade_title": "Det lykkedes ikke at opgradere rummet", "information_section_room": "Information om rummet", - "information_section_space": "Information om gruppen", + "information_section_space": "Information om klyngen", "room_id": "Internt rum-ID", "room_predecessor": "Se ældre beskeder i %(roomName)s.", "room_upgrade_button": "Opgrader dette rum til den anbefalede rumversion", @@ -1883,7 +1883,7 @@ "room_version": "Version af rum:", "room_version_section": "Version af rum", "space_predecessor": "Se ældre version af %(spaceName)s.", - "space_upgrade_button": "Opgradér denne gruppe til den anbefalede rumversion", + "space_upgrade_button": "Opgradér denne klynge til den anbefalede rumversion", "unfederated": "Dette rum er ikke tilgængeligt via eksterne Matrix-servere.", "upgrade_button": "Opgrader dette rum til version %(version)s", "upgrade_dialog_description": "For at opgradere dette rum skal den nuværende instans af rummet lukkes, og der skal oprettes et nyt rum i stedet. For at give rummets medlemmer den bedst mulige oplevelse vil vi:", @@ -1927,30 +1927,30 @@ "canonical_alias_field_label": "Hovedadresse", "default_url_previews_off": "URL-forhåndsvisninger er som standard deaktiveret for deltagere i dette rum.", "default_url_previews_on": "URL-forhåndsvisninger er som standard aktiveret for deltagere i dette rum.", - "description_space": "Redigér indstillinger for din gruppe.", + "description_space": "Redigér indstillinger for din klynge.", "error_creating_alias_description": "Der opstod en fejl under oprettelsen af ​​adressen. Den er muligvis ikke tilladt af serveren, eller der er opstået en midlertidig fejl.", "error_creating_alias_title": "Fejl ved oprettelse af adresse", "error_deleting_alias_description": "Der opstod en fejl ved fjernelsen af den adresse. Den findes muligvis ikke længere, eller der opstod en midlertidig fejl.", "error_deleting_alias_description_forbidden": "Du har ikke tilladelse til at slette adressen.", "error_deleting_alias_title": "Fejl ved fjernelse af adresse", - "error_save_space_settings": "Kunne ikke gemme gruppens indstillinger.", + "error_save_space_settings": "Kunne ikke gemme klyngens indstillinger.", "error_updating_alias_description": "Der opstod en fejl under opdateringen af ​​rummets alternative adresser. Serveren tillader det muligvis ikke, eller der er opstået en midlertidig fejl.", "error_updating_canonical_alias_description": "Der opstod en fejl under opdatering af rummets primære adresse. Serveren tillader muligvis ikke opdateringen, eller der er opstået en midlertidig fejl.", "error_updating_canonical_alias_title": "Fejl ved opdatering af hovedadresse", - "leave_space": "Forlad gruppe", + "leave_space": "Forlad klynge", "local_alias_field_label": "Lokal adresse", "local_aliases_explainer_room": "Angiv adresser til dette rum, så brugerne kan finde det via din hjemmeserver (%(localDomain)s )", - "local_aliases_explainer_space": "Angiv adresser til denne gruppe, så brugerne kan finde den via din hjemmeserver (%(localDomain)s )", + "local_aliases_explainer_space": "Angiv adresser til denne klynge, så brugerne kan finde den via din hjemmeserver (%(localDomain)s )", "local_aliases_section": "Lokale adresser", "name_field_label": "Rummets navn", "new_alias_placeholder": "Ny offentliggjort adresse (f.eks. #alias:server)", - "no_aliases_room": "Denne gruppe har ingen lokal adresse", - "no_aliases_space": "Denne gruppe har ingen lokale adresser", + "no_aliases_room": "Denne klynge har ingen lokale adresser", + "no_aliases_space": "Denne klynge har ingen lokale adresser", "other_section": "Andre", "publish_toggle": "Offentliggør dette rum i %(domain)s's oversigt over rum?", "published_aliases_description": "For at offentliggøre en adresse skal den først indstilles som en lokal adresse.", "published_aliases_explainer_room": "Offentliggjorte adresser kan bruges af alle på enhver server til at deltage i dit rum.", - "published_aliases_explainer_space": "Publicerede adresser kan bruges af alle til at tilslutte sig din gruppe.", + "published_aliases_explainer_space": "Publicerede adresser kan bruges af alle til at tilslutte sig din klynge.", "published_aliases_section": "Offentliggjorte adresser", "save": "Gem ændringer", "topic_field_label": "Rummets emne", @@ -1995,13 +1995,13 @@ "m.call.member": "Deltag i %(brand)s opkald", "m.reaction": "Send reaktioner", "m.room.avatar": "Skift rummets avatar", - "m.room.avatar_space": "Skift gruppens avatar", + "m.room.avatar_space": "Skift klyngens avatar", "m.room.canonical_alias": "Skift hovedadresse for rummet", - "m.room.canonical_alias_space": "Skift hovedadresse for gruppen", + "m.room.canonical_alias_space": "Skift hovedadresse for klyngen", "m.room.encryption": "Aktivér kryptering af rummet", "m.room.history_visibility": "Skift historikkens synlighed", "m.room.name": "Skift rummets navn", - "m.room.name_space": "Skift gruppens navn", + "m.room.name_space": "Skift klyngens navn", "m.room.pinned_events": "Administrer fastgjorte begivenheder", "m.room.power_levels": "Skift tilladelser", "m.room.redaction": "Fjern beskeder sendt af mig", @@ -2009,14 +2009,14 @@ "m.room.tombstone": "Opgrader rummet", "m.room.topic": "Skift emne", "m.room.topic_space": "Lav ændringer i beskrivelsen", - "m.space.child": "Administrer rummene i denne gruppe", + "m.space.child": "Administrer rummene i denne klynge", "m.widget": "Rediger widgets", "muted_users_section": "Brugere, der er slået fra", "no_privileged_users": "Ingen brugere har særlige rettigheder i dette rum.", "notifications.room": "Underret alle", "permissions_section": "Tilladelser", "permissions_section_description_room": "Vælg de roller, der kræves for at ændre forskellige dele af rummet", - "permissions_section_description_space": "Indstil de nødvendige roller der er påkrævet for at kunne ændre på forskellige dele af gruppen", + "permissions_section_description_space": "Vælg de roller der er påkrævet for at kunne ændre på forskellige dele af klyngen", "privileged_users_section": "Privilegerede brugere", "redact": "Fjern beskeder sendt af andre", "send_event_type": "Sende %(eventType)s begivenheder", @@ -2049,20 +2049,20 @@ "join_rule_knock": "Bed om at deltage", "join_rule_knock_description": "Andre kan ikke tilslutte sig, medmindre der gives adgang.", "join_rule_public_description": "Enhver kan finde og tilslutte sig.", - "join_rule_restricted": "Medlemmer af gruppen", - "join_rule_restricted_description": "Alle i en gruppe kan finde og deltage. Rediger hvilke grupper der har adgang her.", - "join_rule_restricted_description_active_space": "Enhver i kan finde og deltage. Du kan også vælge andre grupper.", - "join_rule_restricted_description_prompt": "Alle i en gruppe kan finde og deltage. Du kan vælge flere grupper.", - "join_rule_restricted_description_spaces": "Grupper med adgang", - "join_rule_restricted_dialog_description": "Bestem hvilke grupper der har adgang til dette rum. Hvis en gruppe bliver valgt, vil dens medlemmer kunne finde og deltage i .", - "join_rule_restricted_dialog_empty_warning": "Du er i færd med at fjerne alle grupper. Adgang vil falde tilbage på: \"kun for inviterede\".", - "join_rule_restricted_dialog_filter_placeholder": "Søg i grupper", - "join_rule_restricted_dialog_heading_known": "Andre grupper du kender", - "join_rule_restricted_dialog_heading_other": "Andre grupper eller rum, du måske ikke kender til", - "join_rule_restricted_dialog_heading_room": "Grupper, du kender, der indeholder dette rum", - "join_rule_restricted_dialog_heading_space": "Grupper, du kender, der indeholder denne gruppe", + "join_rule_restricted": "Medlemmer af klyngen", + "join_rule_restricted_description": "Alle, der er i en klynge med tilladelse, kan finde og deltage. Håndter klynger.", + "join_rule_restricted_description_active_space": "Enhver i kan finde og deltage", + "join_rule_restricted_description_prompt": "Alle i en klynge kan deltage.", + "join_rule_restricted_description_spaces": "Autoriserede klynger", + "join_rule_restricted_dialog_description": "Klynger, hvor medlemmer kan tilmelde sig uden en invitation.", + "join_rule_restricted_dialog_empty_warning": "Du er i færd med at fjerne alle uautoriserede klynger. Adgang vil falde tilbage på: \"kun for inviterede\".", + "join_rule_restricted_dialog_filter_placeholder": "Søg i klynger", + "join_rule_restricted_dialog_heading_known": "Dine klynger, der ikke indeholder dette rum", + "join_rule_restricted_dialog_heading_other": "Andre klynger, du ikke er medlem af", + "join_rule_restricted_dialog_heading_room": "Dine klynger, der indeholder dette rum", + "join_rule_restricted_dialog_heading_space": "Klynger, du kender, der indeholder denne klynge", "join_rule_restricted_dialog_heading_unknown": "Det er sandsynligvis nogle, som andre rumadministratorer er en del af.", - "join_rule_restricted_dialog_title": "Udvælg grupper", + "join_rule_restricted_dialog_title": "Administrér klynger", "join_rule_restricted_n_more": { "one": "& %(count)s mere", "other": "& %(count)s mere" @@ -2071,8 +2071,8 @@ "one": "I øjeblikket har et rum adgang", "other": "I øjeblikket har %(count)s rum adgang" }, - "join_rule_restricted_upgrade_description": "Denne opgradering vil tillade medlemmer af udvalgte grupper at tilgå dette rum uden invitation.", - "join_rule_restricted_upgrade_warning": "Dette rum er i nogle grupper, som du ikke er admin i. I de rum, vil det gamle rum stadig blive vist, men brugere vil blive foreslået at tilslutte sig det nye.", + "join_rule_restricted_upgrade_description": "Denne opgradering vil tillade medlemmer af udvalgte klynger at tilgå dette rum uden invitation.", + "join_rule_restricted_upgrade_warning": "Dette rum er i nogle klynger, som du ikke er admin for. I de klynger, vil det gamle rum stadig blive vist, men brugere vil blive foreslået at tilslutte sig det nye.", "join_rule_upgrade_awaiting_room": "Indlæser nyt rum", "join_rule_upgrade_required": "Opgradering påkrævet", "join_rule_upgrade_sending_invites": { @@ -2080,13 +2080,13 @@ "other": "Sender invitationer ... (%(progress)s ud af %(count)s)" }, "join_rule_upgrade_updating_spaces": { - "one": "Opdaterer gruppe...", - "other": "Opdaterer grupper... (%(progress)s ud af %(count)s)" + "one": "Opdaterer klynge...", + "other": "Opdaterer klynger... (%(progress)s ud af %(count)s)" }, "join_rule_upgrade_upgrading_room": "Opgradering af rum", "public_without_alias_warning": "For at kunne linke til dette rum, bedes du tilføje en adresse.", "publish_room": "Gør dette rum synligt i det offentlige rum-oversigt.", - "publish_space": "Gør denne gruppe synlig i det offentlige register.", + "publish_space": "Gør denne klynge synlig i det offentlige register.", "strict_encryption": "Send kun beskeder til verificerede brugere", "title": "Sikkerhed & Privatliv" }, @@ -2094,14 +2094,14 @@ "upload_avatar_label": "Upload avatar", "visibility": { "alias_section": "Adresse", - "error_failed_save": "Kunne ikke opdatere visningen af denne gruppe", + "error_failed_save": "Kunne ikke opdatere visningen af denne klynge", "error_update_guest_access": "Det lykkedes ikke at opdatere gæsteadgangen til dette rum.", - "error_update_history_visibility": "Det lykkedes ikke at opdatere synligheden af historikken i denne gruppe.", - "guest_access_explainer": "Gæster kan tilslutte sig en gruppe uden at have en konto. Dette kan være nyttigt i offentlige grupper.", + "error_update_history_visibility": "Det lykkedes ikke at opdatere synligheden af historikken i denne klynge.", + "guest_access_explainer": "Gæster kan tilslutte sig en klynge uden at have en konto. Dette kan være nyttigt i offentlige klynger.", "guest_access_label": "Aktivér gæsteadgang", - "history_visibility_anyone_space": "Forhåndsvis gruppe", - "history_visibility_anyone_space_description": "Tillad andre at få forhåndsvist din gruppe før de tilslutter sig.", - "history_visibility_anyone_space_recommendation": "Anbefalet for offentlige grupper.", + "history_visibility_anyone_space": "Forhåndsvis klynge", + "history_visibility_anyone_space_description": "Tillad andre at få forhåndsvist din klynge før de tilslutter sig.", + "history_visibility_anyone_space_recommendation": "Anbefalet for offentlige klynger.", "title": "Synlighed" }, "voip": { @@ -2114,14 +2114,14 @@ "room_summary_card_back_action_label": "Informationer om rummet", "scalar": { "error_create": "Kunne ikke lave widget.", - "error_membership": "Du er ikke i denne gruppe.", + "error_membership": "Du er ikke i dette rum.", "error_missing_room_id": "roomId mangler.", "error_missing_room_id_request": "Mangler room_id i forespørgsel", "error_missing_user_id_request": "Manglende user_id i forespørgsel", - "error_permission": "Du har ikke tilladelse til at gøre dét i denne gruppe.", + "error_permission": "Du har ikke tilladelse til at gøre dét i dette rum.", "error_power_level_invalid": "Magtniveau skal være positivt heltal.", - "error_room_not_visible": "Gruppe %(roomId)s ikke synlig", - "error_room_unknown": "Denne gruppe kan ikke genkendes.", + "error_room_not_visible": "Rum %(roomId)s ikke synlig", + "error_room_unknown": "Dette rum kan ikke genkendes.", "error_send_request": "Kunne ikke sende forespørgsel.", "failed_read_event": "Kunne ikke læse begivenheder", "failed_send_event": "Lykkedes ikke med at sende hændelse!" @@ -2555,16 +2555,16 @@ "sidebar": { "metaspaces_favourites_description": "Saml alle dine yndlingsrum og -personer på ét sted.", "metaspaces_home_all_rooms": "Vis alle rum", - "metaspaces_home_all_rooms_description": "Vi alle dine rum i Hjem, selv hvis de er i en gruppe", + "metaspaces_home_all_rooms_description": "Vi alle dine rum i Hjem, selv hvis de er i en klynge", "metaspaces_home_description": "Hjem er godt til at få et overblik over det hele.", - "metaspaces_orphans": "Rum udenfor en gruppe", - "metaspaces_orphans_description": "Saml alle dine grupper, der ikke er en del af et rum, på ét sted.", + "metaspaces_orphans": "Rum udenfor en klynge", + "metaspaces_orphans_description": "Saml alle dine rum, der ikke er en del af en klynge, på ét sted.", "metaspaces_people_description": "Saml alle dine forbindelser på ét sted.", - "metaspaces_subsection": "Grupper at vise", + "metaspaces_subsection": "Klynger at vise", "metaspaces_video_rooms": "Video-rum og konferencer", "metaspaces_video_rooms_description": "Saml alle private video-rum og konferencer.", "metaspaces_video_rooms_description_invite_extension": "I konferencer kan du invitere andre, der ikke er på matrix.", - "spaces_explainer": "Grupper er en måde at samle rum og brugere på. Ud over de grupper du er i, kan du også benytte nogle nogle forhåndskonstruerede.", + "spaces_explainer": "Klynger er en måde at samle rum og brugere på. Ud over de klynger du er i, kan du også benytte nogle nogle forhåndskonstruerede.", "title": "Sidemenu" }, "start_automatically": { @@ -2705,37 +2705,37 @@ "one": "Tilføjer rum...", "other": "Tilføjer rum... (%(progress)s ud af %(count)s)" }, - "space_dropdown_label": "Valg af gruppe", + "space_dropdown_label": "Valg af klynge", "space_dropdown_title": "Tilføj eksisterende rum", - "subspace_moved_note": "Tilføjelse af grupper er flyttet" + "subspace_moved_note": "Tilføjelse af klynger er flyttet" }, "add_existing_subspace": { - "create_button": "Opret en ny gruppe", - "create_prompt": "Vil du tilføje en ny gruppe i stedet?", - "filter_placeholder": "Søg efter grupper", - "space_dropdown_title": "Tilføj eksisterende gruppe" + "create_button": "Opret en ny klynge", + "create_prompt": "Vil du tilføje en ny klynge i stedet?", + "filter_placeholder": "Søg efter klynger", + "space_dropdown_title": "Tilføj eksisterende klynge" }, "context_menu": { "devtools_open_timeline": "Se rummets tidslinje (devtools)", "explore": "Udforsk rum", - "home": "Hjem for gruppen", + "home": "Hjem for klynge", "manage_and_explore": "Bestyr og udforsk rum", - "options": "Valgmuligheder for gruppe" + "options": "Valgmuligheder for klynge" }, "failed_load_rooms": "Kunne ikke indlæse listen over rum.", "failed_remove_rooms": "Kunne ikke fjerne nogle rum. Prøv igen senere.", - "incompatible_server_hierarchy": "Din server understøtter ikke visning af gruppehierarkier.", + "incompatible_server_hierarchy": "Din server understøtter ikke visning af klyngehierakier.", "invite": "Invitér brugere", "invite_description": "Invitér med email eller brugernavn", "invite_link": "Del invitationslink", "joining_space": "Tilslutter", "landing_welcome": "Velkommen til ", - "leave_dialog_action": "Forlad gruppe", + "leave_dialog_action": "Forlad klynge", "leave_dialog_description": "Du er i færd med at forlade .", - "leave_dialog_only_admin_room_warning": "Du er den eneste admin i nogle af de rum eller grupper som du ønsker at forlade. Hvis du forlader dem, står de uden nogen administratorer.", - "leave_dialog_only_admin_warning": "Du er den eneste administrator af denne gruppe. Hvis du forlader den, medfører det, at ingen længere har kontrol over den.", + "leave_dialog_only_admin_room_warning": "Du er den eneste admin i nogle af de rum eller klynger som du ønsker at forlade. Hvis du forlader dem, står de uden nogen administratorer.", + "leave_dialog_only_admin_warning": "Du er den eneste administrator af denne klynge. Hvis du forlader den, medfører det, at ingen længere har kontrol over den.", "leave_dialog_option_all": "Forlad alle rum", - "leave_dialog_option_intro": "Ønsker du at forlade rummene i denne gruppe?", + "leave_dialog_option_intro": "Ønsker du at forlade rummene i denne klynge?", "leave_dialog_option_none": "Forlad ikke nogen rum", "leave_dialog_option_specific": "Forlad visse rum", "leave_dialog_public_rejoin_warning": "Du kan ikke tilslutte dig igen, medmindre du modtager en ny invitation.", @@ -2744,17 +2744,17 @@ "no_search_result_hint": "Du kan prøve med en anden søgning eller tjekke denne for stavefejl.", "preferences": { "sections_section": "Sektioner, der skal vises", - "show_people_in_space": "Dette samler dine chats med medlemmer af denne gruppe. Hvis du slår denne funktion fra, bliver disse chats skjult fra din visning af %(spaceName)s." + "show_people_in_space": "Dette samler dine chats med medlemmer af denne klynge. Hvis du slår denne funktion fra, bliver disse chats skjult fra din visning af %(spaceName)s." }, "room_filter_placeholder": "Søg efter rum", "search_children": "Søg i %(spaceName)s", "search_placeholder": "Søg efter navne og beskrivelser", "select_room_below": "Vælg først et rum nedenfor", - "share_public": "Del din offentlige gruppe", + "share_public": "Del din offentlige klynge", "suggested": "Forslag", "suggested_tooltip": "Dette rum anbefales som et godt et at deltage i", "title_when_query_available": "Resultater", - "title_when_query_unavailable": "Rum og grupper", + "title_when_query_unavailable": "Rum og klynger", "unmark_suggested": "Markér som ikke foreslået", "user_lacks_permission": "Du har ikke tilladelse" }, @@ -2762,10 +2762,10 @@ "title": "Indstillinger - %(spaceName)s" }, "spaces": { - "error_no_permission_add_room": "Du har ikke tilladelse til at tilføje rum til denne gruppe", - "error_no_permission_add_space": "Du har ikke tilladelse til at tilføje andre grupper til denne gruppe.", - "error_no_permission_create_room": "Du har ikke tilladelse til at oprette nye rum i denne gruppe", - "error_no_permission_invite": "Du har ikke tilladelse til at invitere brugere til denne gruppe" + "error_no_permission_add_room": "Du har ikke tilladelse til at tilføje rum til denne klynge", + "error_no_permission_add_space": "Du har ikke tilladelse til at tilføje andre klynger til denne klynge", + "error_no_permission_create_room": "Du har ikke tilladelse til at oprette nye rum i denne klynge", + "error_no_permission_invite": "Du har ikke tilladelse til at invitere brugere til denne klynge" }, "spotlight": { "public_rooms": { @@ -2793,7 +2793,7 @@ }, "create_new_room_button": "Opret nyt rum", "failed_querying_public_rooms": "Kunne ikke forespørge på offentlige rum", - "failed_querying_public_spaces": "Kunne ikke forespørge på offentlige grupper", + "failed_querying_public_spaces": "Kunne ikke forespørge på offentlige klynger", "group_chat_section_title": "Andre indstillinger", "heading_with_query": "Brug \"%(query)s\" til at søge", "heading_without_query": "Søg efter", @@ -2801,14 +2801,14 @@ "keyboard_scroll_hint": "Brug til at rulle", "other_rooms_in_space": "Andre rum i %(spaceName)s", "public_rooms_label": "Offentlige rum", - "public_spaces_label": "Offentlige grupper", + "public_spaces_label": "Offentlige klynger", "recent_searches_section_title": "Seneste søgninger", "recently_viewed_section_title": "Senest besøgt", "remove_filter": "Fjern søgefilter for %(filter)s", "result_may_be_hidden_privacy_warning": "Nogle resultater kan være skjult af hensyn til privatlivets fred", "result_may_be_hidden_warning": "Nogle resultater kan være skjult", "search_dialog": "Dialogboks til søgning", - "spaces_title": "Grupper, du er med i", + "spaces_title": "Klynger, du er med i", "start_group_chat_button": "Start en fælles samtale" }, "stickers": { @@ -2968,12 +2968,12 @@ }, "m.room.canonical_alias": { "alt_added": { - "one": "%(senderName)s tilføjede alternative adresser %(addresses)s til denne gruppe.", - "other": "%(senderName)s tilføjede de alternative adresser %(addresses)s til denne gruppe." + "one": "%(senderName)s tilføjede en alternativ adresse %(addresses)s til dette rum.", + "other": "%(senderName)s tilføjede de alternative adresser %(addresses)s til denne dette rum." }, "alt_removed": { - "one": "%(senderName)s fjernede alternative adresser %(addresses)s til denne gruppe.", - "other": "%(senderName)s fjernede de alternative adresser %(addresses)s til denne gruppe." + "one": "%(senderName)s fjernede alternative adresser %(addresses)s til dette rum.", + "other": "%(senderName)s fjernede de alternative adresser %(addresses)s til dette rum." }, "changed": "%(senderName)s ændrede adresserne til dette rum.", "changed_alternative": "%(senderName)s ændrede de alternative adresser til dette rum.", @@ -3318,7 +3318,7 @@ "user_info": { "admin_tools_section": "Administrationsværktøjer", "ban_button_room": "Spær fra rum", - "ban_button_space": "Spær fra gruppe", + "ban_button_space": "Spær fra klynge", "ban_room_confirm_title": "Spær fra %(roomName)s", "ban_space_everything": "Spær dem fra alle de steder hvor jeg kan", "ban_space_specific": "Spær dem fra specifikke steder hvor jeg kan", @@ -3331,7 +3331,7 @@ "demote_self_confirm_title": "Nedgrader dig selv?", "disinvite_button_room": "Fjern invitation til rum", "disinvite_button_room_name": "Fjern invitation til %(roomName)s", - "disinvite_button_space": "Tilbagekald invitation til gruppen", + "disinvite_button_space": "Tilbagekald invitation til klyngen", "error_ban_user": "Kunne ikke spærre bruger", "error_deactivate": "Det lykkedes ikke at deaktivere brugeren", "error_kicking_user": "Brugeren kunne ikke fjernes", @@ -3344,7 +3344,7 @@ "jump_to_rr_button": "Gå til læsekvittering", "kick_button_room": "Fjern fra rummet", "kick_button_room_name": "Fjern fra %(roomName)s", - "kick_button_space": "Spær fra gruppe", + "kick_button_space": "Fjern fra klynge", "kick_button_space_everything": "Fjern dem fra alle de steder, hvor jeg har beføjelser til det", "kick_space_specific": "Fjern dem fra specifikke steder, hvor jeg har beføjelser til det", "kick_space_warning": "De vil stadig have adgang til de steder, du ikke er administrator for.", @@ -3373,7 +3373,7 @@ "room_unencrypted_detail": "I krypterede rum er dine beskeder sikret, og kun du og modtageren har de unikke nøgler til at låse dem op.", "share_button": "Del profil", "unban_button_room": "Fjern brugerens spærring fra rummet", - "unban_button_space": "Fjern brugerens spærring fra gruppen", + "unban_button_space": "Fjern brugerens spærring fra klyngen", "unban_room_confirm_title": "Fjern brugerens spærring fra %(roomName)s", "unban_space_everything": "Fjern spærring af dem alle steder hvor jeg kan", "unban_space_specific": "Fjern spærring af dem fra specifikke ting, jeg er i stand til", diff --git a/apps/web/src/viewmodels/room/WidgetPipViewModel.tsx b/apps/web/src/viewmodels/room/WidgetPipViewModel.tsx new file mode 100644 index 0000000000..e3deb31c4d --- /dev/null +++ b/apps/web/src/viewmodels/room/WidgetPipViewModel.tsx @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { + BaseViewModel, + type WidgetPipViewSnapshot, + type WidgetPipViewModel as WidgetPipViewModelInterface, +} from "@element-hq/web-shared-components"; +import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; +import React from "react"; + +import type { RefObject, FC } from "react"; +import { Action } from "../../dispatcher/actions"; +import WidgetStore, { type IApp } from "../../stores/WidgetStore"; +import { CallStore, CallStoreEvent } from "../../stores/CallStore"; +import { type Call } from "../../models/Call"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; +import PersistentApp from "../../components/views/elements/PersistentApp"; + +export interface Props { + /** + * The widgetId this widget PiP view is showing. + */ + widgetId: string; + /** + * The room this widget PiP view model is associated with. + */ + room: Room; + /** + * A callback which is called when a mouse event (most likely mouse down) occurs at the start of moving the PiP around. + */ + onStartMoving: (ev: React.MouseEvent) => void; + /** + * This callback ref will be used by the ViewModel once the view is moving. + * Widgets might be implemented with a top-layer DOM tree path containing the widget iframe. + * This allows moving the iframe around (PiP/in-room) without remounting it. + * This callback allows any `PersistentApp` view/component to know when to update the iframe position of the widget. + */ + movePersistedElement: RefObject<(() => void) | null>; +} + +export class WidgetPipViewModel + extends BaseViewModel + implements WidgetPipViewModelInterface +{ + /** The widget this view model uses for the PipView */ + private readonly widget: IApp; + /** + * The call associated with the widget (if the widget is a call widget) + * For non-call widgets, this will be `null`. + */ + private call: Call | null; + /** If the user is currently viewing the room associated with the PiP view (`this.props.room`) */ + private viewingRoom?: boolean; + + public constructor(props: Props) { + super(props, { widgetId: props.widgetId, roomName: props.room.name, roomId: props.room.roomId }); + this.widget = WidgetStore.instance.getApps(props.room.roomId).find((app) => app.id === this.props.widgetId)!; + this.call = CallStore.instance.getCall(props.room.roomId) ?? null; + this.onStartMoving = props.onStartMoving; + + this.disposables.trackListener(props.room, RoomEvent.Name, this.onRoomName); + this.disposables.trackListener(CallStore.instance, CallStoreEvent.Call, this.onCallChange); + } + + public onStartMoving: (ev: React.MouseEvent) => void; + + /** + * The view model needs to know if the room is currently being viewed. + * @param viewing Whether we are currently viewing the room. + */ + public setViewingRoom(viewing: boolean): void { + this.viewingRoom = viewing; + } + + public onBackClick(ev: React.MouseEvent): void { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.call !== null) { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: this.props.room.roomId, + view_call: true, + metricsTrigger: "WebFloatingCallWindow", + }); + } else if (this.viewingRoom) { + WidgetLayoutStore.instance.moveToContainer(this.props.room, this.widget, Container.Center); + } else { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: this.props.room.roomId, + metricsTrigger: "WebFloatingCallWindow", + }); + } + } + + /** + * The component to render as the persistent app by the WidgetPipView. + * @param props A copy of the `PersistentApp` component's props. + * @returns + */ + public persistentAppComponent: FC< + Pick, "persistentWidgetId" | "persistentRoomId"> + > = (props) => { + return ( + + ); + }; + + private readonly onRoomName = (): void => { + this.snapshot.merge({ roomName: this.props.room.name }); + }; + + private readonly onCallChange = (...args: unknown[]): void => { + const [call, forRoomId] = args as [Call | null, string]; + if (forRoomId === this.props.room.roomId) { + this.call = call?.widget.id === this.props.widgetId ? call : null; + } + }; +} diff --git a/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx b/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx index 35966d6d02..be30707f2e 100644 --- a/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx +++ b/apps/web/test/unit-tests/components/structures/PipContainer-test.tsx @@ -17,7 +17,7 @@ import { RoomStateEvent, type RoomMember, } from "matrix-js-sdk/src/matrix"; -import { Widget, type ClientWidgetApi } from "matrix-widget-api"; +import { Widget } from "matrix-widget-api"; import { useMockedCalls, @@ -47,7 +47,6 @@ import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/Wid import WidgetStore from "../../../../src/stores/WidgetStore"; import { WidgetType } from "../../../../src/widgets/WidgetType"; import { SdkContextClass } from "../../../../src/contexts/SDKContext"; -import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions"; import { type WidgetMessaging } from "../../../../src/stores/widgets/WidgetMessaging"; jest.mock("../../../../src/stores/OwnProfileStore", () => ({ @@ -194,7 +193,7 @@ describe("PipContainer", () => { expect(screen.queryByRole("complementary")).toBeNull(); }); - it("shows an active call with back and leave buttons", async () => { + it("shows an active call with back buttons", async () => { renderPip(); await withCall(async (call) => { @@ -211,11 +210,6 @@ describe("PipContainer", () => { metricsTrigger: expect.any(String), }); defaultDispatcher.unregister(dispatcherRef); - - // The leave button should disconnect from the call - const disconnectSpy = jest.spyOn(call, "disconnect"); - await user.click(screen.getByRole("button", { name: "Leave" })); - expect(disconnectSpy).toHaveBeenCalled(); }); }); @@ -252,16 +246,7 @@ describe("PipContainer", () => { mockPlatformPeg({ supportsJitsiScreensharing: () => true }); setUpRoomViewStore(); viewRoom(room2.roomId); - const widget = WidgetStore.instance.addVirtualWidget( - { - id: "1", - creatorUserId: "@alice:example.org", - type: WidgetType.JITSI.preferred, - url: "https://meet.example.org", - name: "Jitsi example", - }, - room.roomId, - ); + renderPip(); await withWidget(async () => { @@ -277,25 +262,6 @@ describe("PipContainer", () => { metricsTrigger: expect.any(String), }); defaultDispatcher.unregister(dispatcherRef); - - // The leave button should hangup the call - const sendSpy = jest - .fn< - ReturnType, - Parameters - >() - .mockResolvedValue({}); - const mockMessaging = { - on: () => {}, - off: () => {}, - stop: () => {}, - widgetApi: { - transport: { send: sendSpy }, - }, - } as unknown as WidgetMessaging; - WidgetMessagingStore.instance.storeMessaging(new Widget(widget), room.roomId, mockMessaging); - await user.click(screen.getByRole("button", { name: "Leave" })); - expect(sendSpy).toHaveBeenCalledWith(ElementWidgetActions.HangupCall, {}); }); WidgetStore.instance.removeVirtualWidget("1", room.roomId); diff --git a/apps/web/test/unit-tests/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap b/apps/web/test/unit-tests/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap index 3d214ab98c..9020570c2d 100644 --- a/apps/web/test/unit-tests/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap +++ b/apps/web/test/unit-tests/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap @@ -3,7 +3,7 @@ exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 and 2 should render both contents 1`] = `