diff --git a/customisations.json b/customisations.json new file mode 100644 index 0000000000..12a5840f48 --- /dev/null +++ b/customisations.json @@ -0,0 +1,6 @@ +{ + "src/@types/tchap.ts": "src/@types/tchap.ts", + "src/components/views/dialogs/CreateRoomDialog.tsx": "src/components/views/dialogs/TchapCreateRoomDialog.tsx", + "src/components/views/elements/TchapRoomTypeSelector.tsx": "src/components/views/elements/TchapRoomTypeSelector.tsx", + "res/css/views/elements/_TchapRoomTypeSelector.scss": "res/css/views/elements/_TchapRoomTypeSelector.scss" +} diff --git a/res/css/views/elements/_TchapRoomTypeSelector.scss b/res/css/views/elements/_TchapRoomTypeSelector.scss new file mode 100644 index 0000000000..99587afc48 --- /dev/null +++ b/res/css/views/elements/_TchapRoomTypeSelector.scss @@ -0,0 +1,55 @@ +.mx_LayoutSwitcher { + .mx_LayoutSwitcher_RadioButtons { + display: flex; + flex-direction: row; + gap: 24px; + + color: $primary-content; + + > .mx_LayoutSwitcher_RadioButton { + flex-grow: 0; + flex-shrink: 1; + display: flex; + flex-direction: column; + + width: 300px; + min-width: 0; + + border: 1px solid $appearance-tab-border-color; + border-radius: 10px; + + .mx_MessageActionBar { + display: none; + } + + .mx_LayoutSwitcher_RadioButton_preview { + flex-grow: 1; + display: flex; + align-items: center; + padding: 10px; + pointer-events: none; + } + + .mx_StyledRadioButton { + flex-grow: 0; + padding: 10px; + } + + &.mx_LayoutSwitcher_RadioButton_selected { + border-color: $accent; + } + } + + .mx_StyledRadioButton { + border-top: 1px solid $appearance-tab-border-color; + + > input + div { + border-color: rgba($muted-fg-color, 0.2); + } + } + + .mx_StyledRadioButton_checked { + background-color: rgba($accent, 0.08); + } + } +} diff --git a/src/@types/tchap.ts b/src/@types/tchap.ts new file mode 100644 index 0000000000..fb2a3eff73 --- /dev/null +++ b/src/@types/tchap.ts @@ -0,0 +1,12 @@ +export enum TchapRoomType { + Direct = "direct", + Private = "private", + External = "external", + Forum = "forum", +} + + +export enum TchapRoomAccessRule { + Unrestricted = "unrestricted", // todo not used in this file, we haven't implemented DMs yet + Restricted = "restricted" +} \ No newline at end of file diff --git a/src/components/views/dialogs/TchapCreateRoomDialog.tsx b/src/components/views/dialogs/TchapCreateRoomDialog.tsx new file mode 100644 index 0000000000..6707e7de7a --- /dev/null +++ b/src/components/views/dialogs/TchapCreateRoomDialog.tsx @@ -0,0 +1,242 @@ +/* +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Note on imports : because this file will be copied to a different directory by the customisations +mechanism, imports must use absolute paths. +Except when importing from other customisation files. Then imports must use relative paths. +*/ +import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; + +import withValidation, { IFieldState } from 'matrix-react-sdk/src/components/views/elements/Validation'; +import { _t } from 'matrix-react-sdk/src/languageHandler'; +import { IOpts, privateShouldBeEncrypted } from "matrix-react-sdk/src/createRoom"; +import { getKeyBindingsManager } from "matrix-react-sdk/src/KeyBindingsManager"; +import { KeyBindingAction } from "matrix-react-sdk/src/accessibility/KeyboardShortcuts"; +import { HistoryVisibility, ICreateRoomOpts } from "matrix-js-sdk"; +import * as sdk from 'matrix-react-sdk/src/index'; +import DialogButtons from 'matrix-react-sdk/src/components/views/elements/DialogButtons'; + +import TchapRoomTypeSelector from "./../elements/TchapRoomTypeSelector"; +import { TchapRoomAccessRule, TchapRoomType } from "../../../@types/tchap"; +// todo remove unused imports at the end. + + + + +export interface ITchapCreateRoomOpts extends ICreateRoomOpts{ + accessRule?:TchapRoomAccessRule +} + +interface IProps { + defaultPublic?: boolean; + defaultName?: string; + parentSpace?: Room; + defaultEncrypted?: boolean; + onFinished(proceed: boolean, opts?: IOpts): void; +} + +interface IState { + name: string; + nameIsValid: boolean; + tchapRoomType: TchapRoomType; +} + +export default class TchapCreateRoomDialog extends React.Component { + + private nameField = createRef(); + + constructor(props) { + super(props); + + this.state = { + name: this.props.defaultName || "", + nameIsValid: false, + tchapRoomType: TchapRoomType.Private, + }; + } + + componentDidMount() { + // move focus to first field when showing dialog + this.nameField.current.focus(); + } + + componentWillUnmount() { + } + + private onCancel = () => { + this.props.onFinished(false); + }; + + private onTchapRoomTypeChange = (tchapRoomType: TchapRoomType) => { + this.setState({ tchapRoomType }); + }; + + private onNameChange = (ev: ChangeEvent) => { + this.setState({ name: ev.target.value }); + }; + + private onNameValidate = async (fieldState: IFieldState) => { + const result = await TchapCreateRoomDialog.validateRoomName(fieldState); + this.setState({ nameIsValid: result.valid }); + return result; + }; + + private static validateRoomName = withValidation({ + rules: [ + { + key: "required", + test: async ({ value }) => !!value, + invalid: () => _t("Please enter a name for the room"), + }, + ], + }); + + private onKeyDown = (event: KeyboardEvent) => { + const action = getKeyBindingsManager().getAccessibilityAction(event); + switch (action) { + case KeyBindingAction.Enter: + this.onOk(); + event.preventDefault(); + event.stopPropagation(); + break; + } + }; + + private onOk = async () => { + const activeElement = document.activeElement as HTMLElement; + if (activeElement) { + activeElement.blur(); + } + await this.nameField.current.validate({ allowEmpty: false }); + // Validation and state updates are async, so we need to wait for them to complete + // first. Queue a `setState` callback and wait for it to resolve. + await new Promise(resolve => this.setState({}, resolve)); + if (this.state.nameIsValid) { + this.props.onFinished(true, this.roomCreateOptions(this.state.name, this.state.tchapRoomType)); + } else { + let field; + if (!this.state.nameIsValid) { + field = this.nameField.current; + } + if (field) { + field.focus(); + field.validate({ allowEmpty: false, focused: true }); + } + } + }; + + private roomCreateOptions(name:string,tchapRoomType:TchapRoomType) { + const opts: IOpts = {}; + const createRoomOpts: ITchapCreateRoomOpts = {}; + opts.createOpts = createRoomOpts; + + //tchap common options + createRoomOpts.name = name; + opts.guestAccess = false; //guest access are not authorized in tchap + + //todo: always activate federation within tchap, I guess? + //from ealier tchap -> noFederate: Tchap.getShortDomain() === "Agent" ? false : !this.state.federate, + createRoomOpts.creation_content = { 'm.federate': true }; + + switch(tchapRoomType){ + case TchapRoomType.Forum:{ + //"Forum" only for tchap members and not encrypted + createRoomOpts.accessRule = TchapRoomAccessRule.Restricted; + createRoomOpts.visibility = Visibility.Public; + createRoomOpts.preset = Preset.PublicChat; + opts.joinRule = JoinRule.Public; + opts.encryption = false; + opts.historyVisibility = HistoryVisibility.Shared; + break; + } + case TchapRoomType.Private:{ + + //"Salon", only for tchap member and encrypted + createRoomOpts.accessRule = TchapRoomAccessRule.Restricted; + createRoomOpts.visibility = Visibility.Private; + createRoomOpts.preset = Preset.PrivateChat; + opts.joinRule = JoinRule.Invite + opts.encryption = true; + opts.historyVisibility = HistoryVisibility.Joined; + break; + } + case TchapRoomType.External:{ + + //open to external and encrypted, + createRoomOpts.accessRule = TchapRoomAccessRule.Unrestricted + createRoomOpts.visibility = Visibility.Private; + createRoomOpts.preset = Preset.PrivateChat; + opts.joinRule = JoinRule.Invite + opts.encryption = true; + opts.historyVisibility = HistoryVisibility.Joined; + break; + } + } + return opts; + } + + render() { + + const Field = sdk.getComponent("elements.Field"); + const BaseDialog =sdk.getComponent("dialogs.BaseDialog"); + + const title = _t("Create a room"); + /* todo do we need this ? + if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { + const name = CommunityPrototypeStore.instance.getSelectedCommunityName(); + title = _t("Create a room in %(communityName)s", { communityName: name }); + } else if (!this.props.parentSpace) { + title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room'); + } + */ + + return ( + +
+
+ + + + +
+
+ +
+ ); + } +} diff --git a/src/components/views/elements/TchapRoomTypeSelector.tsx b/src/components/views/elements/TchapRoomTypeSelector.tsx new file mode 100644 index 0000000000..48ea2aa5f0 --- /dev/null +++ b/src/components/views/elements/TchapRoomTypeSelector.tsx @@ -0,0 +1,104 @@ +/* +Copyright 2022 DINUM +*/ + +import React from 'react'; +import classNames from "classnames"; +import { _t } from 'matrix-react-sdk/src/languageHandler'; + +import { TchapRoomType } from "../../../@types/tchap"; +import * as sdk from 'matrix-react-sdk/src/index'; + +interface IProps { + value: TchapRoomType; + label: string; + width?: number; + onChange(value: TchapRoomType): void; +} + +interface IState { + roomType: TchapRoomType; +} + +// todo rename, not a dropdown anymore +export default class TchapRoomTypeSelector extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + roomType: TchapRoomType.Private, + }; + } + + private onRoomTypeChange = (e: React.ChangeEvent): void => { + const roomType = e.target.value as TchapRoomType; + + this.setState({ roomType: roomType }); + this.props.onChange(roomType); + }; + + public render(): JSX.Element { + + const StyledRadioButton = sdk.getComponent("elements.StyledRadioButton"); + + + const ircClasses = classNames("mx_LayoutSwitcher_RadioButton", { + mx_LayoutSwitcher_RadioButton_selected: this.state.roomType == TchapRoomType.Private, + }); + const groupClasses = classNames("mx_LayoutSwitcher_RadioButton", { + mx_LayoutSwitcher_RadioButton_selected: this.state.roomType == TchapRoomType.External, + }); + const bubbleClasses = classNames("mx_LayoutSwitcher_RadioButton", { + mx_LayoutSwitcher_RadioButton_selected: this.state.roomType === TchapRoomType.Forum, + }); + + return
+ + + +
+ ; + } +} diff --git a/src/i18n/strings/tchap_translations.json b/src/i18n/strings/tchap_translations.json index 7b51db1e7b..7b63b36717 100644 --- a/src/i18n/strings/tchap_translations.json +++ b/src/i18n/strings/tchap_translations.json @@ -17,5 +17,29 @@ "Choose a homeserver:": { "en": "Choose a homeserver:", "fr": "Choisissez un serveur d'accueil :" + }, + "Private room": { + "en": "Private room", + "fr": "Salon privé" + }, + "Private room open to external users": { + "en": "Private room open to external users", + "fr": "Salon privé ouvert aux externes" + }, + "Forum room": { + "en": "Forum room", + "fr": "Salon forum" + }, + "Accessible to all users by invitation from an administrator.": { + "en": "Accessible to all users by invitation from an administrator.", + "fr": "Accessible à tous les utilisateurs sur invitation d'un administrateur." + }, + "Accessible to all users and to external guests by invitation of an administrator.": { + "en": "Accessible to all users and to external guests by invitation of an administrator.", + "fr": "Accessible à tous les utilisateurs et aux invités externes sur invitation d'un administrateur." + }, + "Accessible to all users from the forum directory or from a shared link.": { + "en": "Accessible to all users from the forum directory or from a shared link.", + "fr": "Accessible à tous les utilisateurs à partir de la liste des forums ou d'un lien partagé." } }