Add Jitsi Meet Integration ()

* basic integration structure

* jitsi logo

* add jitsi meet description to event settings page

* add JitsiVideoApiAdapter

ref 

* add LocationType.Jitsi to BookingPage

ref 

* add LocationType.Jitsi to event-types

ref 

* add meet.jit.si/cal/uuid support to BookingPage

ref 

* add basic "cal_provide_jitsi_meeting_url" translation strings

ref 

* generate meeting id

ref 

* implement direct jitsi link in /success page

ref 

* cleanup location link duplicate

ref 

* full JitsiVideoApiAdapter implementation

ref 

* check integration availability in /pages/event-types/[type]

ref 

* add video conferencing link as calendar event location

ref 

* PR feedback

* Update components/booking/pages/BookingPage.tsx

don't know - wouldn't do this myself for future proofing but fine...

Co-authored-by: Omar López <zomars@me.com>

* Update components/booking/pages/BookingPage.tsx

🤷‍♂️

Co-authored-by: Omar López <zomars@me.com>

* cleanup: props.type === "jitsi_video"

ref 

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Philipp Dormann 2022-02-08 23:12:28 +01:00 committed by GitHub
parent 38b41f0adb
commit 20d2955e68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 171 additions and 20 deletions

View file

@ -16,6 +16,7 @@ import { Controller, useForm, useWatch } from "react-hook-form";
import { FormattedNumber, IntlProvider } from "react-intl";
import { ReactMultiEmail } from "react-multi-email";
import { useMutation } from "react-query";
import { v4 as uuidv4 } from "uuid";
import { createPaymentLink } from "@ee/lib/stripe/client";
@ -89,6 +90,9 @@ const BookingPage = (props: BookingPageProps) => {
if (!location) {
return;
}
if (location === "integrations:jitsi") {
return "https://meet.jit.si/cal/" + uuidv4();
}
if (location.includes("integration")) {
return t("web_conferencing_details_to_follow");
}
@ -143,6 +147,7 @@ const BookingPage = (props: BookingPageProps) => {
[LocationType.Phone]: t("phone_call"),
[LocationType.GoogleMeet]: "Google Meet",
[LocationType.Zoom]: "Zoom Video",
[LocationType.Jitsi]: "Jitsi Meet",
[LocationType.Daily]: "Daily.co Video",
[LocationType.Huddle01]: "Huddle01 Video",
[LocationType.Tandem]: "Tandem Video",
@ -330,6 +335,12 @@ const BookingPage = (props: BookingPageProps) => {
{getLocationValue({ locationType: selectedLocation })}
</p>
)}
{selectedLocation === LocationType.Jitsi && (
<p className="mb-2 text-gray-500">
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
Jitsi Meet
</p>
)}
<p className="mb-4 text-green-500">
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
{parseDate(date)}

View file

@ -66,6 +66,7 @@ export const getLocationRequestFromIntegration = (location: string) => {
location === LocationType.GoogleMeet.valueOf() ||
location === LocationType.Zoom.valueOf() ||
location === LocationType.Daily.valueOf() ||
location === LocationType.Jitsi.valueOf() ||
location === LocationType.Huddle01.valueOf() ||
location === LocationType.Tandem.valueOf()
) {

View file

@ -14,6 +14,8 @@ export function getIntegrationName(name: string) {
return "Apple Calendar";
case "daily_video":
return "Daily";
case "jitsi_video":
return "Jitsi Meet";
case "huddle01_video":
return "Huddle01";
case "tandem_video":

View file

@ -0,0 +1,34 @@
import { v4 as uuidv4 } from "uuid";
import { PartialReference } from "@lib/events/EventManager";
import { VideoApiAdapter, VideoCallData } from "@lib/videoClient";
const JitsiVideoApiAdapter = (): VideoApiAdapter => {
return {
getAvailability: () => {
return Promise.resolve([]);
},
createMeeting: async (): Promise<VideoCallData> => {
const meetingID = uuidv4();
return Promise.resolve({
type: "jitsi_video",
id: meetingID,
password: "",
url: "https://meet.jit.si/cal/" + meetingID,
});
},
deleteMeeting: async (): Promise<void> => {
Promise.resolve();
},
updateMeeting: (bookingRef: PartialReference): Promise<VideoCallData> => {
return Promise.resolve({
type: "jitsi_video",
id: bookingRef.meetingId as string,
password: bookingRef.meetingPassword as string,
url: bookingRef.meetingUrl as string,
});
},
};
};
export default JitsiVideoApiAdapter;

View file

@ -1,11 +1,7 @@
import { Prisma } from "@prisma/client";
import _ from "lodash";
/**
* We can't use aliases in playwright tests (yet)
* https://github.com/microsoft/playwright/issues/7121
*/
import { validJson } from "../../lib/jsonUtils";
import { validJson } from "@lib/jsonUtils";
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
select: { id: true, type: true },
@ -24,6 +20,7 @@ export type Integration = {
| "caldav_calendar"
| "apple_calendar"
| "stripe_payment"
| "jitsi_video"
| "huddle01_video"
| "metamask_web3";
title: string;
@ -65,6 +62,14 @@ export const ALL_INTEGRATIONS = [
description: "Video Conferencing",
variant: "conferencing",
},
{
installed: true,
type: "jitsi_video",
title: "Jitsi Meet",
imageSrc: "integrations/jitsi.svg",
description: "Video Conferencing",
variant: "conferencing",
},
{
installed: true,
type: "huddle01_video",
@ -146,7 +151,10 @@ export function hasIntegration(integrations: IntegrationMeta, type: string): boo
(i) =>
i.type === type &&
!!i.installed &&
(type === "daily_video" || type === "huddle01_video" || i.credentials.length > 0)
(type === "daily_video" ||
type === "jitsi_video" ||
type === "huddle01_video" ||
i.credentials.length > 0)
);
}
export function hasIntegrationInstalled(type: Integration["type"]): boolean {

View file

@ -4,6 +4,7 @@ export enum LocationType {
GoogleMeet = "integrations:google:meet",
Zoom = "integrations:zoom",
Daily = "integrations:daily",
Jitsi = "integrations:jitsi",
Huddle01 = "integrations:huddle01",
Tandem = "integrations:tandem",
}

View file

@ -6,6 +6,7 @@ import { getUid } from "@lib/CalEventParser";
import { EventResult } from "@lib/events/EventManager";
import { PartialReference } from "@lib/events/EventManager";
import Huddle01VideoApiAdapter from "@lib/integrations/Huddle01/Huddle01VideoApiAdapter";
import JitsiVideoApiAdapter from "@lib/integrations/Jitsi/JitsiVideoApiAdapter";
import logger from "@lib/logger";
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
@ -46,6 +47,9 @@ const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] =>
case "daily_video":
acc.push(DailyVideoApiAdapter(cred));
break;
case "jitsi_video":
acc.push(JitsiVideoApiAdapter());
break;
case "huddle01_video":
acc.push(Huddle01VideoApiAdapter());
break;

View file

@ -115,6 +115,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const defaultLocations = [
{ value: LocationType.InPerson, label: t("in_person_meeting") },
{ value: LocationType.Jitsi, label: "Jitsi Meet" },
{ value: LocationType.Phone, label: t("phone_call") },
];
@ -125,7 +126,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const updateMutation = trpc.useMutation("viewer.eventTypes.update", {
onSuccess: async ({ eventType }) => {
await router.push("/event-types");
showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
showToast(
t("event_type_updated_successfully", {
eventTypeTitle: eventType.title,
}),
"success"
);
},
onError: (err) => {
if (err instanceof HttpError) {
@ -261,6 +267,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
return <p className="text-sm">{t("cal_provide_zoom_meeting_url")}</p>;
case LocationType.Daily:
return <p className="text-sm">{t("cal_provide_video_meeting_url")}</p>;
case LocationType.Jitsi:
return <p className="text-sm">{t("cal_provide_jitsi_meeting_url")}</p>;
case LocationType.Huddle01:
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
case LocationType.Tandem:
@ -276,7 +284,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
setCustomInputs([...customInputs]);
};
const schedulingTypeOptions: { value: SchedulingType; label: string; description: string }[] = [
const schedulingTypeOptions: {
value: SchedulingType;
label: string;
description: string;
}[] = [
{
value: SchedulingType.COLLECTIVE,
label: t("collective"),
@ -328,7 +340,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
locations: { type: LocationType; address?: string }[];
customInputs: EventTypeCustomInput[];
users: string[];
availability: { openingHours: AvailabilityInput[]; dateOverrides: AvailabilityInput[] };
availability: {
openingHours: AvailabilityInput[];
dateOverrides: AvailabilityInput[];
};
timeZone: string;
periodType: PeriodType;
periodDays: number;
@ -548,6 +563,33 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<span className="ml-2 text-sm">Tandem Video</span>
</div>
)}
{location.type === LocationType.Jitsi && (
<div className="flex items-center flex-grow">
<svg
className="w-6 h-6"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M32 0C49.6733 0 64 14.3267 64 32C64 49.6733 49.6733 64 32 64C14.3267 64 0 49.6733 0 32C0 14.3267 14.3267 0 32 0Z"
fill="#E5E5E4"
/>
<path
d="M32.0002 0.623047C49.3292 0.623047 63.3771 14.6709 63.3771 31.9999C63.3771 49.329 49.3292 63.3768 32.0002 63.3768C14.6711 63.3768 0.623291 49.329 0.623291 31.9999C0.623291 14.6709 14.6716 0.623047 32.0002 0.623047Z"
fill="white"
/>
<path
d="M31.9998 3.14014C47.9386 3.14014 60.8597 16.0612 60.8597 32C60.8597 47.9389 47.9386 60.8599 31.9998 60.8599C16.0609 60.8599 3.13989 47.9389 3.13989 32C3.13989 16.0612 16.0609 3.14014 31.9998 3.14014Z"
fill="#4A8CFF"
/>
<path
d="M13.1711 22.9581V36.5206C13.1832 39.5875 15.6881 42.0558 18.743 42.0433H38.5125C39.0744 42.0433 39.5266 41.5911 39.5266 41.0412V27.4788C39.5145 24.4119 37.0096 21.9435 33.9552 21.956H14.1857C13.6238 21.956 13.1716 22.4082 13.1716 22.9581H13.1711ZM40.7848 28.2487L48.9469 22.2864C49.6557 21.6998 50.2051 21.8462 50.2051 22.9095V41.0903C50.2051 42.2999 49.5329 42.1536 48.9469 41.7134L40.7848 35.7631V28.2487Z"
fill="white"
/>
</svg>
<span className="ml-2 text-sm">Jitsi Meet</span>
</div>
)}
<div className="flex">
<button
type="button"
@ -1350,7 +1392,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
.findIndex((loc) => values.locationType === loc.type);
if (existingIdx !== -1) {
const copy = formMethods.getValues("locations");
copy[existingIdx] = { ...formMethods.getValues("locations")[existingIdx], ...details };
copy[existingIdx] = {
...formMethods.getValues("locations")[existingIdx],
...details,
};
formMethods.setValue("locations", copy);
} else {
formMethods.setValue(
@ -1604,17 +1649,36 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const locationOptions: OptionTypeBase[] = [];
if (hasIntegration(integrations, "zoom_video")) {
locationOptions.push({ value: LocationType.Zoom, label: "Zoom Video", disabled: true });
locationOptions.push({
value: LocationType.Zoom,
label: "Zoom Video",
disabled: true,
});
}
const hasPaymentIntegration = hasIntegration(integrations, "stripe_payment");
if (hasIntegration(integrations, "google_calendar")) {
locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" });
locationOptions.push({
value: LocationType.GoogleMeet,
label: "Google Meet",
});
}
if (hasIntegration(integrations, "daily_video")) {
locationOptions.push({ value: LocationType.Daily, label: "Daily.co Video" });
locationOptions.push({
value: LocationType.Daily,
label: "Daily.co Video",
});
}
if (hasIntegration(integrations, "jitsi_video")) {
locationOptions.push({
value: LocationType.Jitsi,
label: "Jitsi Meet",
});
}
if (hasIntegration(integrations, "huddle01_video")) {
locationOptions.push({ value: LocationType.Huddle01, label: "Huddle01 Video" });
locationOptions.push({
value: LocationType.Huddle01,
label: "Huddle01 Video",
});
}
if (hasIntegration(integrations, "tandem_video")) {
locationOptions.push({ value: LocationType.Tandem, label: "Tandem Video" });

View file

@ -480,7 +480,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
);
}
/** We don't need to "Connect", just show that it's installed */
if (props.type === "daily_video" || props.type === "huddle01_video") {
if (["daily_video", "huddle01_video", "jitsi_video"].includes(props.type)) {
return (
<div className="px-3 py-2 truncate">
<h3 className="text-sm font-medium text-gray-700">{t("installed")}</h3>

View file

@ -32,7 +32,8 @@ dayjs.extend(timezone);
export default function Success(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const { location, name, reschedule } = router.query;
const { location: _location, name, reschedule } = router.query;
const location = Array.isArray(_location) ? _location[0] : _location;
const [is24h, setIs24h] = useState(false);
const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date)));
@ -58,7 +59,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
function eventLink(): string {
const optional: { location?: string } = {};
if (location) {
optional["location"] = Array.isArray(location) ? location[0] : location;
optional["location"] = location;
}
const event = createEvent({
@ -141,7 +142,15 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
{location && (
<>
<div className="font-medium">{t("where")}</div>
<div className="col-span-2">{location}</div>
<div className="col-span-2">
{location.startsWith("http") ? (
<a title="Meeting Link" href={location}>
{location}
</a>
) : (
location
)}
</div>
</>
)}
</div>
@ -149,7 +158,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
</div>
{!needsConfirmation && (
<div className="flex pt-2 pb-4 mt-5 text-center border-b dark:border-gray-900 sm:mt-0 sm:pt-4">
<span className="flex self-center ltr:mr-2 rtl:ml-2 font-medium text-gray-700 dark:text-gray-50">
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
{t("add_to_calendar")}
</span>
<div className="flex justify-center flex-grow text-center">
@ -162,7 +171,8 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
.utc()
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
props.eventType.description
}` + (typeof location === "string" ? encodeURIComponent(location) : "")
}` +
(typeof location === "string" ? "&location=" + encodeURIComponent(location) : "")
}>
<a className="w-10 h-10 px-3 py-2 mx-2 border rounded-sm border-neutral-200 dark:border-neutral-700 dark:text-white">
<svg

File diff suppressed because one or more lines are too long

After

(image error) Size: 17 KiB

View file

@ -553,6 +553,7 @@
"cal_provide_zoom_meeting_url": "Cal poskytne URL Zoom meetingu.",
"cal_provide_tandem_meeting_url": "Cal poskytne URL Tandem meetingu.",
"cal_provide_video_meeting_url": "Cal poskytne URL Daily video meetingu.",
"cal_provide_jitsi_meeting_url": "Cal poskytne URL Jitsi Meet video meetingu.",
"cal_provide_huddle01_meeting_url": "Cal poskytne URL Huddle01 web3 video meetingu.",
"require_payment": "Vyžadovat platbu",
"commission_per_transaction": "provize za transakci",

View file

@ -557,6 +557,7 @@
"cal_provide_zoom_meeting_url": "Cal stellt eine Zoom Meeting-URL zur Verfügung.",
"cal_provide_tandem_meeting_url": "Cal stellt eine Tandem Meeting-URL zur Verfügung.",
"cal_provide_video_meeting_url": "Cal stellt eine tägliche Video-Meeting-URL zur Verfügung.",
"cal_provide_jitsi_meeting_url": "Cal stellt eine Jitsi Meet URL zur Verfügung.",
"cal_provide_huddle01_meeting_url": "Cal stellt eine tägliche Huddle01-Web3-Meeting-URL zur Verfügung.",
"require_payment": "Zahlung erforderlich",
"commission_per_transaction": "Provision pro Transaktion",

View file

@ -559,6 +559,7 @@
"cal_provide_zoom_meeting_url": "Cal will provide a Zoom meeting URL.",
"cal_provide_tandem_meeting_url": "Cal will provide a Tandem meeting URL.",
"cal_provide_video_meeting_url": "Cal will provide a Daily video meeting URL.",
"cal_provide_jitsi_meeting_url": "We will generate a Jitsi Meet URL for you.",
"cal_provide_huddle01_meeting_url": "Cal will provide a Huddle01 web3 video meeting URL.",
"require_payment": "Require Payment",
"commission_per_transaction": "commission per transaction",

View file

@ -525,6 +525,7 @@
"cal_provide_zoom_meeting_url": "Cal proporcionará una URL de reunión de Zoom.",
"cal_provide_tandem_meeting_url": "Cal proporcionará una URL de reunión de Tandem.",
"cal_provide_video_meeting_url": "Cal proporcionará una URL de reunión de Daily Video.",
"cal_provide_jitsi_meeting_url": "Cal proporcionará una URL de reunión de Jitsi Meet.",
"cal_provide_huddle01_meeting_url": "Cal proporcionará una URL de reunión de Huddle01 Web3 Video.",
"require_payment": "Requiere Pago",
"commission_per_transaction": "Comisión por Transacción",

View file

@ -491,6 +491,7 @@
"cal_provide_zoom_meeting_url": "Cal fournira une URL de réunion Zoom.",
"cal_provide_tandem_meeting_url": "Cal fournira une URL de réunion Tandem.",
"cal_provide_video_meeting_url": "Cal fournira une URL de réunion Daily video.",
"cal_provide_jitsi_meeting_url": "Cal fournira une URL de réunion Jitsi Meet.",
"cal_provide_huddle01_meeting_url": "Cal fournira une URL de réunion Huddle01 web3 video.",
"require_payment": "Exiger un paiement",
"commission_per_transaction": "commission par transaction",

View file

@ -517,6 +517,7 @@
"cal_provide_zoom_meeting_url": "Cal fornirà un URL di riunione Zoom.",
"cal_provide_tandem_meeting_url": "Cal fornirà un URL di riunione Tandem.",
"cal_provide_video_meeting_url": "Cal fornirà un URL di riunione Daily video.",
"cal_provide_jitsi_meeting_url": "Cal fornirà un URL di riunione Jitsi Meet.",
"cal_provide_huddle01_meeting_url": "Cal fornirà un URL di riunione Huddle01 web3 video.",
"require_payment": "Richiedi Pagamento",
"commission_per_transaction": "commissione per transazione",

View file

@ -489,6 +489,7 @@
"cal_provide_zoom_meeting_url": "カルはZoomミーティングURLを提供します。",
"cal_provide_tandem_meeting_url": "カルはTandemミーティングURLを提供します。",
"cal_provide_video_meeting_url": "カルは毎日のビデオミーティングのURLを提供します。",
"cal_provide_jitsi_meeting_url": "カルはJitsi MeetミーティングURLを提供します。",
"cal_provide_huddle01_meeting_url": "カルはHuddle01 Web3ミーティングURLを提供します。",
"require_payment": "お支払いが必要です",
"commission_per_transaction": "取引あたりの手数料",

View file

@ -512,6 +512,7 @@
"cal_provide_zoom_meeting_url": "Cal은 Zoom 회의 URL을 제공합니다.",
"cal_provide_tandem_meeting_url": "Cal은 Tandem 회의 URL을 제공합니다.",
"cal_provide_video_meeting_url": "Cal은 일일 화상 회의 URL을 제공합니다.",
"cal_provide_jitsi_meeting_url": "Cal은 Jitsi Meet 회의 URL을 제공합니다.",
"cal_provide_huddle01_meeting_url": "Cal은 Huddle01 Web3 회의 URL을 제공합니다.",
"require_payment": "지불 요청",
"commission_per_transaction": "거래당 수수료",

View file

@ -482,6 +482,7 @@
"cal_provide_zoom_meeting_url": "Cal zal een Zoom meeting-URL meegeven in de afspraak bevestiging.",
"cal_provide_tandem_meeting_url": "Cal zal een Tandem meeting-URL meegeven in de afspraak bevestiging.",
"cal_provide_video_meeting_url": "Cal zal een Daily meeting-URL meegeven in de afspraak bevestiging.",
"cal_provide_jitsi_meeting_url": "Cal zal een Jitsi Meet meeting-URL meegeven in de afspraak bevestiging.",
"cal_provide_huddle01_meeting_url": "Cal zal een Huddle01 web3 meeting-URL meegeven in de afspraak bevestiging.",
"require_payment": "Betaling vereisen",
"commission_per_transaction": "commissie per transactie",

View file

@ -527,6 +527,7 @@
"cal_provide_google_meet_location": "Cal zapewni lokalizację Google Meet.",
"cal_provide_zoom_meeting_url": "Cal zapewni URL spotkania Zoom.",
"cal_provide_video_meeting_url": "Kal poda adres URL spotkania wideo platformy Daily.",
"cal_provide_jitsi_meeting_url": "Cal zapewni URL spotkania Jitsi Meet.",
"cal_provide_huddle01_meeting_url": "Cal zapewni URL spotkania Huddle01 Web3.",
"require_payment": "Wymagaj płatności",
"commission_per_transaction": "prowizja za transakcję",

View file

@ -528,6 +528,7 @@
"cal_provide_zoom_meeting_url": "Cal fornecerá uma URL de reunião do Zoom.",
"cal_provide_tandem_meeting_url": "Cal fornecerá uma URL de reunião do Tandem.",
"cal_provide_video_meeting_url": "O Cal irá fornecer um URL de reunião do Daily video.",
"cal_provide_jitsi_meeting_url": "O Cal irá fornecer um URL de reunião do Jitsi Meet.",
"cal_provide_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião do Huddle01 Web3 video.",
"require_payment": "Requerer Pagamento",
"commission_per_transaction": "comissão por transação",

View file

@ -559,6 +559,7 @@
"cal_provide_zoom_meeting_url": "O Cal irá fornecer um URL de reunião do Zoom.",
"cal_provide_tandem_meeting_url": "O Cal irá fornecer um URL de reunião do Tandem.",
"cal_provide_video_meeting_url": "O Cal irá fornecer um URL de reunião do Daily video.",
"cal_provide_jitsi_meeting_url": "O Cal irá fornecer um URL de reunião vídeo do Jitsi Meet.",
"cal_provide_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião vídeo do Huddle01 Web3.",
"require_payment": "Requer Pagamento",
"commission_per_transaction": "comissão por transação",

View file

@ -489,6 +489,7 @@
"cal_provide_zoom_meeting_url": "Cal va oferi un URL pentru ședința de Zoom.",
"cal_provide_tandem_meeting_url": "Cal va oferi un URL pentru ședința de Tandem.",
"cal_provide_video_meeting_url": "Cal va oferi un URL pentru ședința de Daily.",
"cal_provide_jitsi_meeting_url": "Cal va oferi un URL pentru ședința de Jitsi Meet.",
"cal_provide_huddle01_meeting_url": "Cal va oferi un URL pentru ședința de Huddle01 Web3.",
"require_payment": "Solicită plata",
"commission_per_transaction": "comision per tranzacție",

View file

@ -529,6 +529,7 @@
"cal_provide_zoom_meeting_url": "Cal создаст ссылку на встречу в Zoom.",
"cal_provide_tandem_meeting_url": "Cal создаст ссылку на встречу в Tandem.",
"cal_provide_video_meeting_url": "Cal создаст ссылку на встречу в Daily.",
"cal_provide_jitsi_meeting_url": "Cal создаст ссылку на встречу в Jitsi Meet.",
"cal_provide_huddle01_meeting_url": "Cal создаст ссылку на встречу в Huddle01 Web3.",
"require_payment": "Требуется оплата",
"commission_per_transaction": "комиссия за сделку",

View file

@ -534,6 +534,7 @@
"cal_provide_google_meet_location": "Cal 将提供 Google Meet 位置。",
"cal_provide_zoom_meeting_url": "Cal 将提供Zoom会议 URL",
"cal_provide_video_meeting_url": "Cal 将提供Daily视频会议 URL",
"cal_provide_jitsi_meeting_url": "Cal 将提供Jitsi Meet视频会议 URL",
"cal_provide_huddle01_meeting_url": "Cal 将提供Huddle01 Web3视频会议 URL",
"require_payment": "需要付款",
"commission_per_transaction": "每笔交易的佣金",