Added translation to all molecules

This commit is contained in:
Dylan 2022-07-14 19:24:19 +09:30
parent 71bc3ea41e
commit 3668342ca7
29 changed files with 717 additions and 214 deletions

View file

@ -334,6 +334,18 @@
"shared_power_message": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?",
"demoting_self_message": "You will not be able to undo this change as you are demoting yourself. Are you sure?"
},
"PeopleDrawer": {
"title": "People",
"members_one": "{{count}} member",
"members_other": "{{count}} members",
"joined": "Joined",
"invited": "Invited",
"banned": "Banned",
"search_no_results": "No results found",
"view_more": "View more",
"placeholder": "Search",
"invite_tooltip": "Invite"
},
"ProfileEditor": {
"remove_avatar": "Remove avatar",
"remove_avatar_confirmation": "Are you sure that you want to remove your avatar?",
@ -439,5 +451,326 @@
"create_space": "Create space",
"home": "Home"
}
},
"Molecules":{
"ConfirmDialog":{
"cancel": "Cancel"
},
"ReusableDialog":
{
"close_tooltip": "Close"
},
"FollowingMembers":{
"users_following_one": "<bold>{{user_one}}</bold> is following the conversation",
"users_following_two": "<bold>{{user_one}}</bold> and <bold>{{user_two}}</bold> are following the conversation",
"users_following_three": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, and <bold>{{user_three}}</bold> are following the conversation",
"users_following_other": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, <bold>{{user_three}}</bold> and {{other_count}} others are following the conversation"
},
"ImageUpload":
{
"prompt": "Upload",
"cancel": "Cancel",
"remove": "Remove"
},
"ExportE2ERoomKeys":
{
"getting_keys": "Getting keys...",
"encrypting_keys": "Encrypting Keys...",
"password_does_not_match": "Password does not match.",
"export_success": "Successfully exported all keys.",
"export_failed": "Failed to export keys. Please try again.",
"button_text": "Export"
},
"ImportE2ERoomKeys":
{
"decrypting_file": "Decrypting file...",
"decrypting_messages": "Decrypting messages...",
"import_success": "Successfully imported all keys",
"import_failed": "Failed tp decrypt keys. Please try again",
"import_keys_button": "Import Keys",
"decrypt_button": "Decrypt"
},
"Media":{
"open_new_tab": "Open in new tab",
"download": "Download",
"play_audio": "Play audio",
"play_video": "Play video"
},
"Message":
{
"message_deleted": "*** This message has been deleted ***",
"unable_to_load_reply": "*** Unable to load reply ***",
"unknown_user": "** Unknown user **",
"edited": "(edited)",
"edit_placeholder": "Edit message",
"user_reacted_one": "<bold>{{user_one}}</bold> reacted with <emoji/>",
"user_reacted_two": "<bold>{{user_one}}</bold> and <bold>{{user_two}}</bold> reacted with <emoji/>",
"user_reacted_three": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold> and <bold>{{user_three}}</bold> reacted with <emoji/>",
"user_reacted_other": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, <bold>{{user_three}}</bold> and {{other_count}} others reacted with <emoji/>",
"add_reaction_tooltip": "Add reaction",
"reply_tooltip": "Reply",
"edit_tooltip": "Edit",
"options_header": "Options",
"options_tooltip": "Options",
"read_receipts": "Read receipts",
"view_source": "View source",
"delete_message_prompt": "Delete message",
"delete_message_confirmation": "Are you sure that you want to delete this message?",
"delete_message_button": "Delete"
},
"PopupWindow":
{
"close_tooltip": "Back"
},
"PowerLevelSelector": {
"placeholder": "Power level"
},
"RoomAliases": {
"invalid_characters": "Invalid character: only letter, numbers and _- are allowed.",
"validating_alias": "validating {{alias}}...",
"alias_available": "{{alias}} is available.",
"alias_unavailable": "{{alias}} is unavailable.",
"deleting_alias": "Deleting...",
"set_main_alias": "Set as Main",
"publish_alias": "Publish",
"unpublish_alias": "Unpublish",
"delete_alias": "Delete",
"main_alias": "Main",
"publish_to_room_directory":
{
"title": "Publish to room directory",
"publish_room_message": "Publish this room to the {{homeserver}} publish directory?",
"publish_space_message": "Publish this space to the {{homeserver}} publish directory?"
},
"published_addresses":
{
"title": "Published addresses",
"none": "No published addresses",
"no_main_address": "No main address (select one from below)",
"message_room": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.",
"message_space": "Published addresses can be used by anyone on any server to join your space. To publish an address, it needs to be set as a local address first."
},
"local_addresses":
{
"title": "Local addresses",
"none": "No local addresses",
"message_room": "Set local addresses for this room so users can find this room through your homeserver.",
"message_space": "Set local addresses for this space so users can find this space through your homeserver.",
"add": "Add local address",
"add_button": "Add",
"placeholder_room": "my_room_address",
"placeholder_space": "my_space_address",
"hide": "Hide local addresses",
"view": "View local addresses"
}
},
"RoomEncryption":{
"encryption_public_room_message": "It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone should be able to read messages in them.",
"encryption_message": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly",
"encryption_cannot_be_disabled": "Once enabled, encryption cannot be disabled.",
"enable_room_encryption": "Enable room encryption",
"enable_encryption_prompt": "Enable Encryption",
"enable_encryption_button": "Enable",
"continue_button": "Continue"
},
"RoomHistoryVisibility": {
"world_readable": "Anyone (including guests)",
"shared": "Members (all messages)",
"invited": "Members (messages after invite)",
"joined": "Members (messages after join)",
"changes_only_affect_future": "Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect."
},
"RoomMembers": {
"search_title": "Search member",
"found_members_one": "Found — one member",
"found_members_other": "Found — {{count}} members",
"no_results": "No results found for {{term}}",
"no_members": "No members to display",
"view_more": "View more",
"joined": "Joined",
"invited": "Invited",
"banned": "Banned",
"search_placeholder": "Search for name"
},
"RoomNotification":
{
"default": "Global",
"all_messages": "All messages",
"mentions_and_keywords": "Mentions & Keywords",
"mute": "Mute"
},
"RoomOptions":{
"title": "Options for {{room_name}}",
"leave": {
"title": "Leave room",
"subtitle": "Are you sure you want to leave the {{room_name}} room?",
"button_text": "Leave"
},
"mark_as_read": "Mark as read",
"notifications_heading": "Notifications",
"invite": "Invite"
},
"RoomPermissions":
{
"default_role": {
"name": "Default role",
"description": "Set default role for all members"
},
"send_messages": {
"name": "Send messages",
"description": "Set minimum power level to send messages in a room"
},
"reactions":{
"name": "Send reactions",
"description": "Set minimum power level to send reactions in a room"
},
"delete": {
"name": "Delete messages sent by others",
"description": "Set minumum power level to delete messages in a room"
},
"notifications":{
"name": "Ping room",
"description": "Set minimum power level to ping room"
},
"manage_rooms":{
"name": "Manage rooms in space",
"description": "Set minimum power level to manage rooms in space"
},
"invite": {
"name": "Invite",
"description": "Set minimum power level to invite members"
},
"kick": {
"name": "Kick",
"description": "Set minimum power level to kick members"
},
"ban": {
"name": "Ban",
"description": "Set minimum power level to ban members"
},
"change_avatar": {
"name": "Change avatar",
"description": "Set minimum power level to change room/space avatar"
},
"change_name": {
"name": "Change name",
"description": "Set minimum power evel to change room/space name"
},
"change_topic": {
"name": "Change topic",
"description": "Set minimum power level to change room/space topic"
},
"change_settings": {
"name": "Change settings",
"description": "Set minimum power level to change settings"
},
"change_published_address": {
"name": "Change published address",
"description": "Set minimum power level to publish and set main address"
},
"change_permissions": {
"name": "Change permissions",
"description": "Set minimum power level to change permissions"
},
"enable_room_encryption": {
"name": "Enable room encryption",
"description": "Set minimum power level to enable room encryption"
},
"change_history_visibility": {
"name": "Change history visibility",
"description": "Set minimum power level to change room message history visibility"
},
"upgrade_room": {
"name": "Upgrade room",
"description": "Set minimum power level to upgrade room"
},
"pin_messages": {
"name": "Pin messages",
"description": "Set minimum power level to pin messages in a room"
},
"change_acls": {
"name": "Change server ACLs",
"description": "Set minimum power level to change server ACLs"
},
"modify_widgets": {
"name": "Modify widgets",
"description": "Set minimum power level to modify room widgets"
},
"groups":
{
"general": "General permissions",
"manage_members": "Manage members permissions",
"room": "Room profile permissions",
"space": "Space profile permissions",
"other": "Other permissions",
"settings": "Settings permissions"
}
},
"RoomProfile": {
"saving_room_name": "Saving room name...",
"saving_room_topic": "Saving room topic...",
"save_success": "Saved successfully",
"save_failed": "Unable to save",
"remove_avatar_title": "Remove avatar",
"remove_avatar_subtitle": "Are you sure that you want to remove room avatar?",
"remove_avatar_button": "Remove",
"permission_change_room_name": "You have permission to change room name only",
"permission_change_room_topic": "You have permission to change room topic only",
"permission_change_space_name": "You have permission to change space name only",
"permission_change_space_topic": "You have permission to change space topic only",
"name_label": "Name",
"topic_label": "Topic"
},
"RoomSearch": {
"title": "Room search",
"placeholder": "Search for keywords",
"search_button": "Search",
"searching": "Searching room messages...",
"subtitle": "Search room messages",
"failed": "Failed to search messages",
"no_results": "No results found",
"encrypted_room": "Search does not work in encrypted room",
"load_more": "Load more",
"results_one": "{{count}} result for {{term}}",
"results_other": "{{count}} results for {{term}}"
},
"RoomTile": {
"invited_by_user_zero": "Invited by {{inviter}} to {{id}}",
"invited_by_user_one": "Invited by {{inviter}} to {{id}} • {{member_count}} member",
"invited_by_user_other": "Invited by {{inviter}} to {{id}} • {{member_count}} members",
"invited_zero": "{{id}}",
"invited_one": "{{id}} • {{member_count}} member",
"invited_other": "{{id}} • {{member_count}} members"
},
"RoomVisibility": {
"private": "Private (invite only)",
"restricted": "Restricted (space members can join)",
"restricted_unsupported": "Restricted (Unsupported: room required upgrade)",
"public": "Public (anyone can join)"
},
"SpaceAddExisting": {
"adding_items_one": "Adding one item...",
"adding_items_other": "Adding {{count}} items...",
"items_selected_one": "{{count}} item selected",
"items_selected_other": "{{count}} items selected",
"search_rooms_placeholder": "Search rooms",
"no_results": "No results found",
"add_button": "Add",
"subtitle": "add existing rooms"
},
"SpaceOptions": {
"leave_space": "Leave Space",
"leave_space_confirmation": "Are you sure that you want to leave the {{space}} space?",
"leave_space_confirm": "Leave",
"invite": "Invite",
"manage_rooms":"Manage rooms",
"settings": "Settings",
"leave": "Leave"
},
"SSOButtons": {
"login_with": "Login with {{idp_name}}"
}
}
}

View file

@ -7,15 +7,20 @@ import { openReusableDialog } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function ConfirmDialog({
desc, actionTitle, actionType, onComplete,
}) {
const { t } = useTranslation();
return (
<div className="confirm-dialog">
<Text>{desc}</Text>
<div className="confirm-dialog__btn">
<Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button>
<Button onClick={() => onComplete(false)}>Cancel</Button>
<Button onClick={() => onComplete(false)}>{t("Molecules.ConfirmDialog.cancel")}</Button>
</div>
</div>
);

View file

@ -8,10 +8,15 @@ import Dialog from './Dialog';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function ReusableDialog() {
const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState(null);
const { t } = useTranslation();
useEffect(() => {
const handleOpen = (title, render, afterClose) => {
setIsOpen(true);
@ -38,7 +43,7 @@ function ReusableDialog() {
title={data?.title || ''}
onAfterClose={handleAfterClose}
onRequestClose={handleRequestClose}
contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip="Close" />}
contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip={t("Molecules.ReusableDialog.close_tooltip")} />}
invisibleScroll
>
{data?.render(handleRequestClose) || <div />}

View file

@ -13,12 +13,22 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { getUsersActionJsx } from '../../organisms/room/common';
import { twemojify } from '../../../util/twemojify';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
import { getUserDisplayName } from '../../../util/matrixUtil';
function FollowingMembers({ roomTimeline }) {
const [followingMembers, setFollowingMembers] = useState([]);
const { roomId } = roomTimeline;
const mx = initMatrix.matrixClient;
const { roomsInput } = initMatrix;
const myUserId = mx.getUserId();
const room = mx.getRoom(roomId);
const handleOnMessageSent = () => setFollowingMembers([]);
@ -47,7 +57,19 @@ function FollowingMembers({ roomTimeline }) {
size="extra-small"
src={TickMarkIC}
/>
<Text variant="b2">{getUsersActionJsx(roomId, filteredM, 'following the conversation.')}</Text>
<Text variant="b2">
<Trans
i18nKey="Molecules.FollowingMembers.users_following"
values={{
count: filteredM.length,
user_one: twemojify(getUserDisplayName(room, filteredM?.[0])),
user_two: twemojify(getUserDisplayName(room, filteredM?.[1])),
user_three: twemojify(getUserDisplayName(room, filteredM?.[2])),
other_count: filteredM.length - 3
}}
components={{bold: <b/>}}
/>
</Text>
</button>
);
}

View file

@ -8,12 +8,17 @@ import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
import Spinner from '../../atoms/spinner/Spinner';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function ImageUpload({
text, bgColor, imageSrc, onUpload, onRequestRemove,
}) {
const [uploadPromise, setUploadPromise] = useState(null);
const uploadImageRef = useRef(null);
const { t } = useTranslation();
async function uploadImage(e) {
const file = e.target.files.item(0);
if (file === null) return;
@ -53,7 +58,7 @@ function ImageUpload({
size="large"
/>
<div className={`img-upload__process ${uploadPromise === null ? ' img-upload__process--stopped' : ''}`}>
{uploadPromise === null && <Text variant="b3" weight="bold">Upload</Text>}
{uploadPromise === null && <Text variant="b3" weight="bold">{t("Molecules.ImageUpload.prompt")}</Text>}
{uploadPromise !== null && <Spinner size="small" />}
</div>
</button>
@ -63,7 +68,7 @@ function ImageUpload({
type="button"
onClick={uploadPromise === null ? onRequestRemove : cancelUpload}
>
<Text variant="b3">{uploadPromise ? 'Cancel' : 'Remove'}</Text>
<Text variant="b3">{uploadPromise ? t("Molecules.ImageUpload.cancel") : t("Molecules.ImageUpload.remove")}</Text>
</button>
)}
<input onChange={uploadImage} style={{ display: 'none' }} ref={uploadImageRef} type="file" />

View file

@ -14,6 +14,9 @@ import Spinner from '../../atoms/spinner/Spinner';
import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function ExportE2ERoomKeys() {
const isMountStore = useStore();
const [status, setStatus] = useState({
@ -24,19 +27,21 @@ function ExportE2ERoomKeys() {
const passwordRef = useRef(null);
const confirmPasswordRef = useRef(null);
const { t } = useTranslation();
const exportE2ERoomKeys = async () => {
const password = passwordRef.current.value;
if (password !== confirmPasswordRef.current.value) {
setStatus({
isOngoing: false,
msg: 'Password does not match.',
msg: t("Molecules.ExportE2ERoomKeys.password_does_not_match"),
type: cons.status.ERROR,
});
return;
}
setStatus({
isOngoing: true,
msg: 'Getting keys...',
msg: t("Molecules.ExportE2ERoomKeys.getting_keys"),
type: cons.status.IN_FLIGHT,
});
try {
@ -44,7 +49,7 @@ function ExportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: true,
msg: 'Encrypting keys...',
msg: t("Molecules.ExportE2ERoomKeys.encrypting_keys"),
type: cons.status.IN_FLIGHT,
});
}
@ -56,7 +61,7 @@ function ExportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: false,
msg: 'Successfully exported all keys.',
msg: t("Molecules.ExportE2ERoomKeys.export_success"),
type: cons.status.SUCCESS,
});
}
@ -64,7 +69,7 @@ function ExportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: false,
msg: e.friendlyText || 'Failed to export keys. Please try again.',
msg: e.friendlyText || t("Molecules.ExportE2ERoomKeys.export_failed"),
type: cons.status.ERROR,
});
}
@ -83,7 +88,7 @@ function ExportE2ERoomKeys() {
<form className="export-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); exportE2ERoomKeys(); }}>
<Input forwardRef={passwordRef} type="password" placeholder="Password" required />
<Input forwardRef={confirmPasswordRef} type="password" placeholder="Confirm password" required />
<Button disabled={status.isOngoing} variant="primary" type="submit">Export</Button>
<Button disabled={status.isOngoing} variant="primary" type="submit">{t("Molecules.ExportE2ERoomKeys.button_text")}</Button>
</form>
{ status.type === cons.status.IN_FLIGHT && (
<div className="import-e2e-room-keys__process">

View file

@ -15,6 +15,9 @@ import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function ImportE2ERoomKeys() {
const isMountStore = useStore();
const [keyFile, setKeyFile] = useState(null);
@ -26,13 +29,15 @@ function ImportE2ERoomKeys() {
const inputRef = useRef(null);
const passwordRef = useRef(null);
const { t } = useTranslation();
async function tryDecrypt(file, password) {
try {
const arrayBuffer = await file.arrayBuffer();
if (isMountStore.getItem()) {
setStatus({
isOngoing: true,
msg: 'Decrypting file...',
msg: t("Molecules.ImportE2ERoomKeys.decrypting_file"),
type: cons.status.IN_FLIGHT,
});
}
@ -41,7 +46,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: true,
msg: 'Decrypting messages...',
msg: t("Molecules.ImportE2ERoomKeys.decrypting_messages"),
type: cons.status.IN_FLIGHT,
});
}
@ -49,7 +54,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: false,
msg: 'Successfully imported all keys.',
msg: t("Molecules.ImportE2ERoomKeys.import_success"),
type: cons.status.SUCCESS,
});
inputRef.current.value = null;
@ -59,7 +64,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: false,
msg: e.friendlyText || 'Failed to decrypt keys. Please try again.',
msg: e.friendlyText || t("Molecules.ImportE2ERoomKeys.import_failed"),
type: cons.status.ERROR,
});
}
@ -114,9 +119,9 @@ function ImportE2ERoomKeys() {
<Text>{keyFile.name}</Text>
</div>
)}
{keyFile === null && <Button onClick={() => inputRef.current.click()}>Import keys</Button>}
{keyFile === null && <Button onClick={() => inputRef.current.click()}>{t("Molecules.ImportE2ERoomKeys.import_keys_button")}</Button>}
<Input forwardRef={passwordRef} type="password" placeholder="Password" required />
<Button disabled={status.isOngoing} variant="primary" type="submit">Decrypt</Button>
<Button disabled={status.isOngoing} variant="primary" type="submit">{t("Molecules.ImportE2ERoomKeys.decrypt_button")}</Button>
</form>
{ status.type === cons.status.IN_FLIGHT && (
<div className="import-e2e-room-keys__process">

View file

@ -12,6 +12,9 @@ import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
import PlaySVG from '../../../../public/res/ic/outlined/play.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
// https://github.com/matrix-org/matrix-react-sdk/blob/a9e28db33058d1893d964ec96cd247ecc3d92fc3/src/utils/blobs.ts#L73
const ALLOWED_BLOB_MIMETYPES = [
'image/jpeg',
@ -73,6 +76,8 @@ function FileHeader({
}) {
const [url, setUrl] = useState(null);
const { t } = useTranslation();
async function getFile() {
const myUrl = await getUrl(link, type, file);
setUrl(myUrl);
@ -94,7 +99,7 @@ function FileHeader({
external && (
<IconButton
size="extra-small"
tooltip="Open in new tab"
tooltip={t("Molecules.Media.open_new_tab")}
src={ExternalSVG}
onClick={() => window.open(url || link)}
/>
@ -103,7 +108,7 @@ function FileHeader({
<a href={url || link} download={name} target="_blank" rel="noreferrer">
<IconButton
size="extra-small"
tooltip="Download"
tooltip= {t("Molecules.Media.download")}
src={DownloadSVG}
onClick={handleDownload}
/>
@ -151,6 +156,7 @@ function Image({
}) {
const [url, setUrl] = useState(null);
useEffect(() => {
let unmounted = false;
async function fetchUrl() {
@ -204,12 +210,14 @@ function Audio({
loadAudio();
}
const { t } = useTranslation();
return (
<div className="file-container">
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
<div className="audio-container">
{ url === null && isLoading && <Spinner size="small" /> }
{ url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />}
{ url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip={t("Molecules.Media.play_audio")} src={PlaySVG} />}
{ url !== null && (
/* eslint-disable-next-line jsx-a11y/media-has-caption */
<audio autoPlay controls>
@ -263,6 +271,8 @@ function Video({
loadVideo();
}
const { t } = useTranslation();
return (
<div className="file-container">
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
@ -274,7 +284,7 @@ function Video({
className="video-container"
>
{ url === null && isLoading && <Spinner size="small" /> }
{ url === null && !isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
{ url === null && !isLoading && <IconButton onClick={handlePlayVideo} tooltip={t("Molecules.Media.play_video")} src={PlaySVG} />}
{ url !== null && (
/* eslint-disable-next-line jsx-a11y/media-has-caption */
<video autoPlay controls poster={thumbUrl}>

View file

@ -38,6 +38,11 @@ import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
import { t } from 'i18next';
function PlaceholderMessage() {
return (
<div className="ph-msg">
@ -118,6 +123,7 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
useEffect(() => {
const mx = initMatrix.matrixClient;
const timelineSet = roomTimeline.getUnfilteredTimelineSet();
const loadReply = async () => {
try {
const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
@ -250,7 +256,7 @@ const MessageBody = React.memo(({
)}
{ content }
</div>
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
{ isEdited && <Text className="message__body-edited" variant="b3"><Trans i18nKey={"Molecules.Message.edited"}/></Text>}
</div>
);
});
@ -270,6 +276,8 @@ MessageBody.propTypes = {
function MessageEdit({ body, onSave, onCancel }) {
const editInputRef = useRef(null);
const { t } = useTranslation();
useEffect(() => {
// makes the cursor end up at the end of the line instead of the beginning
editInputRef.current.value = '';
@ -289,14 +297,14 @@ function MessageEdit({ body, onSave, onCancel }) {
forwardRef={editInputRef}
onKeyDown={handleKeyDown}
value={body}
placeholder="Edit message"
placeholder={t("Molecules.Message.edit_placeholder")}
required
resizable
autoFocus
/>
<div className="message__edit-btns">
<Button type="submit" variant="primary">Save</Button>
<Button onClick={onCancel}>Cancel</Button>
<Button type="submit" variant="primary">{t("common.save")}</Button>
<Button onClick={onCancel}>{t("common.cancel")}</Button>
</div>
</form>
);
@ -341,20 +349,21 @@ function pickEmoji(e, roomId, eventId, roomTimeline) {
}
function genReactionMsg(userIds, reaction) {
console.log(reaction);
return (
<>
{userIds.map((userId, index) => (
<React.Fragment key={userId}>
{twemojify(getUsername(userId))}
{index < userIds.length - 1 && (
<span style={{ opacity: '.6' }}>
{index === userIds.length - 2 ? ' and ' : ', '}
</span>
)}
</React.Fragment>
))}
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
{twemojify(reaction, { className: 'react-emoji' })}
<Trans
i18nKey="Molecules.Message.user_reacted"
values={{
count: userIds.length,
user_one: getUsername(userIds?.[0]),
user_two: getUsername(userIds?.[1]),
user_three: getUsername(userIds?.[2]),
other_count: userIds.length - 3,
}}
components={{bold: <b/>, emoji: reaction}}
/>
</>
);
}
@ -518,38 +527,38 @@ const MessageOptions = React.memo(({
onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
src={EmojiAddIC}
size="extra-small"
tooltip="Add reaction"
tooltip={t("Molecules.Message.add_reaction_tooltip")}
/>
)}
<IconButton
onClick={() => reply()}
src={ReplyArrowIC}
size="extra-small"
tooltip="Reply"
tooltip={t("Molecules.Message.reply_tooltip")}
/>
{(senderId === mx.getUserId() && !isMedia(mEvent)) && (
<IconButton
onClick={() => edit(true)}
src={PencilIC}
size="extra-small"
tooltip="Edit"
tooltip={t("Molecules.Message.edit_tooltip")}
/>
)}
<ContextMenu
content={() => (
<>
<MenuHeader>Options</MenuHeader>
<MenuHeader>{t("Molecules.Message.options_header")}</MenuHeader>
<MenuItem
iconSrc={TickMarkIC}
onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))}
>
Read receipts
{t("Molecules.Message.read_receipts")}
</MenuItem>
<MenuItem
iconSrc={CmdIC}
onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
>
View source
{t("Molecules.Message.view_source")}
</MenuItem>
{(canIRedact || senderId === mx.getUserId()) && (
<>
@ -559,9 +568,9 @@ const MessageOptions = React.memo(({
iconSrc={BinIC}
onClick={async () => {
const isConfirmed = await confirmDialog(
'Delete message',
'Are you sure that you want to delete this message?',
'Delete',
t("Molecules.Message.delete_message_prompt"),
t("Molecules.Message.delete_message_confirmation"),
t("Molecules.Message.delete_message_button"),
'danger',
);
if (!isConfirmed) return;
@ -579,7 +588,7 @@ const MessageOptions = React.memo(({
onClick={toggleMenu}
src={VerticalMenuIC}
size="extra-small"
tooltip="Options"
tooltip={t("Molecules.Message.options_tooltip")}
/>
)}
/>

View file

@ -13,6 +13,9 @@ import RawModal from '../../atoms/modal/RawModal';
import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function PWContentSelector({
selected, variant, iconSrc,
type, onClick, children,
@ -56,6 +59,8 @@ function PopupWindow({
const haveDrawer = drawer !== null;
const cTitle = contentTitle !== null ? contentTitle : title;
const { t } = useTranslation();
return (
<RawModal
className={`${className === null ? '' : `${className} `}pw-modal`}
@ -69,7 +74,7 @@ function PopupWindow({
{haveDrawer && (
<div className="pw__drawer">
<Header>
<IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" />
<IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip={t("Molecules.PopupWindow.close_tooltip")}/>
<TitleWrapper>
{
typeof title === 'string'

View file

@ -7,6 +7,9 @@ import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function PowerLevelSelector({
value, max, onSelect,
}) {
@ -16,6 +19,8 @@ function PowerLevelSelector({
onSelect(Number(powerLevel));
};
const { t } = useTranslation();
return (
<div className="power-level-selector">
<MenuHeader>Power level selector</MenuHeader>
@ -25,7 +30,7 @@ function PowerLevelSelector({
defaultValue={value}
type="number"
name="power-level"
placeholder="Power level"
placeholder={t("Molecules.PowerLevelIndicator.placeholder")}
max={max}
autoComplete="off"
required

View file

@ -17,10 +17,16 @@ import SettingTile from '../setting-tile/SettingTile';
import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function useValidate(hsString) {
const [debounce] = useState(new Debounce());
const [validate, setValidate] = useState({ alias: null, status: cons.status.PRE_FLIGHT });
const { t } = useTranslation();
const setValidateToDefault = () => {
setValidate({
alias: null,
@ -37,7 +43,7 @@ function useValidate(hsString) {
setValidate({
alias: null,
status: cons.status.ERROR,
msg: 'Invalid character: only letter, numbers and _- are allowed.',
msg: t("Molecules.RoomAliases.invalid_characters")
});
return false;
}
@ -59,7 +65,7 @@ function useValidate(hsString) {
setValidate({
alias,
status: cons.status.IN_FLIGHT,
msg: `validating ${alias}...`,
msg: t("Molecules.RoomAliases.validating_alias", {alias: alias}),
});
const isValid = await isRoomAliasAvailable(alias);
@ -70,7 +76,7 @@ function useValidate(hsString) {
return {
alias,
status: isValid ? cons.status.SUCCESS : cons.status.ERROR,
msg: isValid ? `${alias} is available.` : `${alias} is already in use.`,
msg: t( isValid ? "Molecules.RoomAliases.alias_available": "Molecules.RoomAliases.alias_unavailable", {alias: alias}),
};
});
}, 600)();
@ -110,6 +116,8 @@ function RoomAliases({ roomId }) {
const canPublishAlias = room.currentState.maySendStateEvent('m.room.canonical_alias', userId);
const { t } = useTranslation();
useEffect(() => isMountedStore.setItem(true), []);
useEffect(() => {
@ -225,7 +233,7 @@ function RoomAliases({ roomId }) {
const handleDeleteAlias = async (alias) => {
try {
setDeleteAlias({ alias, status: cons.status.IN_FLIGHT, msg: 'deleting...' });
setDeleteAlias({ alias, status: cons.status.IN_FLIGHT, msg: t("Molecules.RoomAliases.deleting_alias")});
await mx.deleteAlias(alias);
let { main, published, local } = aliases;
if (published.includes(alias)) {
@ -259,10 +267,10 @@ function RoomAliases({ roomId }) {
return (
<div className="room-aliases__item-btns">
{canPublishAlias && !isMain && <Button onClick={() => handleSetMainAlias(alias)} variant="primary">Set as Main</Button>}
{!isPublished && canPublishAlias && <Button onClick={() => handlePublishAlias(alias)} variant="positive">Publish</Button>}
{isPublished && canPublishAlias && <Button onClick={() => handleUnPublishAlias(alias)} variant="caution">Un-Publish</Button>}
<Button onClick={() => handleDeleteAlias(alias)} variant="danger">Delete</Button>
{canPublishAlias && !isMain && <Button onClick={() => handleSetMainAlias(alias)} variant="primary">{t("Molecules.RoomAliases.set_main_alias")}</Button>}
{!isPublished && canPublishAlias && <Button onClick={() => handlePublishAlias(alias)} variant="positive">{t("Molecules.RoomAliases.publish_alias")}</Button>}
{isPublished && canPublishAlias && <Button onClick={() => handleUnPublishAlias(alias)} variant="caution">{t("Molecules.RoomAliases.unpublish_alias")}</Button>}
<Button onClick={() => handleDeleteAlias(alias)} variant="danger">{t("Molecules.RoomAliases.delete_alias")}</Button>
</div>
);
};
@ -278,7 +286,7 @@ function RoomAliases({ roomId }) {
<Checkbox variant="positive" disabled={disabled} isActive={isActive} onToggle={() => handleAliasSelect(alias)} />
<Text>
{alias}
{isMain && <span>Main</span>}
{isMain && <span>{t("Molecules.RoomAliases.main_alias")}</span>}
</Text>
</div>
{isActive && renderAliasBtns(alias)}
@ -292,8 +300,8 @@ function RoomAliases({ roomId }) {
return (
<div className="room-aliases">
<SettingTile
title="Publish to room directory"
content={<Text variant="b3">{`Publish this ${room.isSpaceRoom() ? 'space' : 'room'} to the ${hsString}'s public room directory?`}</Text>}
title={t("Molecules.RoomAliases.publish_to_room_directory.title")}
content={<Text variant="b3">{t(room.isSpaceRoom() ? "Molecules.RoomAliases.publish_to_room_directory.publish_space_message" : "Molecules.RoomAliases.publish_to_room_directory.publish_room_message", {homeserver: hsString})}</Text>}
options={(
<Toggle
isActive={isPublic}
@ -304,35 +312,35 @@ function RoomAliases({ roomId }) {
/>
<div className="room-aliases__content">
<MenuHeader>Published addresses</MenuHeader>
{(aliases.published.length === 0) && <Text className="room-aliases__message">No published addresses</Text>}
{(aliases.published.length > 0 && !aliases.main) && <Text className="room-aliases__message">No Main address (select one from below)</Text>}
<MenuHeader>{t("Molecules.RoomAliases.published_addresses.title")}</MenuHeader>
{(aliases.published.length === 0) && <Text className="room-aliases__message">{t("Molecules.RoomAliases.published_addresses.none")}</Text>}
{(aliases.published.length > 0 && !aliases.main) && <Text className="room-aliases__message">{t("Molecules.RoomAliases.published_addresses.no_main_address")}</Text>}
{aliases.published.map(renderAlias)}
<Text className="room-aliases__message" variant="b3">
{`Published addresses can be used by anyone on any server to join your ${room.isSpaceRoom() ? 'space' : 'room'}. To publish an address, it needs to be set as a local address first.`}
{t(room.isSpaceRoom() ? "Molecules.RoomAliases.published_addresses.message_space" : "Molecules.RoomAliases.published_addresses.message_room")}
</Text>
</div>
{ isLocalVisible && (
<div className="room-aliases__content">
<MenuHeader>Local addresses</MenuHeader>
{(aliases.local.length === 0) && <Text className="room-aliases__message">No local addresses</Text>}
<MenuHeader>{t("Molecules.RoomAliases.local_addresses.title")}</MenuHeader>
{(aliases.local.length === 0) && <Text className="room-aliases__message">{t("Molecules.RoomAliases.local_addresses.none")}</Text>}
{aliases.local.map(renderAlias)}
<Text className="room-aliases__message" variant="b3">
{`Set local addresses for this ${room.isSpaceRoom() ? 'space' : 'room'} so users can find this ${room.isSpaceRoom() ? 'space' : 'room'} through your homeserver.`}
{t(room.isSpaceRoom() ? "Molecules.RoomAliases.local_addresses.message_space" : "Molecules.RoomAliases.local_addresses.message_room" )}
</Text>
<Text className="room-aliases__form-label" variant="b2">Add local address</Text>
<Text className="room-aliases__form-label" variant="b2">{t("Molecules.RoomAliases.local_addresses.add")}</Text>
<form className="room-aliases__form" onSubmit={handleAliasSubmit}>
<div className="room-aliases__input-wrapper">
<Input
name="alias-input"
state={inputState}
onChange={handleAliasChange}
placeholder={`my_${room.isSpaceRoom() ? 'space' : 'room'}_address`}
placeholder={t(room.isSpaceRoom() ? "Molecules.RoomAliases.local_addresses.placeholder_space" : "Molecules.RoomAliases.local_addresses.placeholder_room")}
required
/>
</div>
<Button variant="primary" type="submit">Add</Button>
<Button variant="primary" type="submit">{t("Molecules.RoomAliases.local_addresses.add_button")}</Button>
</form>
<div className="room-aliases__input-status">
{validate.status === cons.status.SUCCESS && <Text className="room-aliases__valid" variant="b2">{validate.msg}</Text>}
@ -342,7 +350,7 @@ function RoomAliases({ roomId }) {
)}
<div className="room-aliases__content">
<Button onClick={() => setIsLocalVisible(!isLocalVisible)}>
{`${isLocalVisible ? 'Hide' : 'Add / View'} local address`}
{t(isLocalVisible ? "Molecules.RoomAliases.local_addresses.hide" : "Molecules.RoomAliases.local_addresses.view" )}
</Button>
</div>
</div>

View file

@ -10,6 +10,9 @@ import SettingTile from '../setting-tile/SettingTile';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function RoomEncryption({ roomId }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
@ -17,16 +20,18 @@ function RoomEncryption({ roomId }) {
const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId());
const { t } = useTranslation();
const handleEncryptionEnable = async () => {
const joinRule = room.getJoinRule();
const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.';
const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly';
const confirmMsg1 = t("Molecules.RoomEncryption.encryption_public_room_message");
const confirmMsg2 = t("Molecules.RoomEncryption.encryption_message");
const isConfirmed1 = (joinRule === 'public')
? await confirmDialog('Enable encryption', confirmMsg1, 'Continue', 'caution')
? await confirmDialog(t("Molecules.RoomEncryption.enable_encryption_prompt"), confirmMsg1, t("Molecules.RoomEncryption.continue_button"), 'caution')
: true;
if (!isConfirmed1) return;
if (await confirmDialog('Enable encryption', confirmMsg2, 'Enable', 'caution')) {
if (await confirmDialog(t("Molecules.RoomEncryption.enable_encryption_prompt"), confirmMsg2, t("Molecules.RoomEncryption.enable_encryption_button"), 'caution')) {
setIsEncrypted(true);
mx.sendStateEvent(roomId, 'm.room.encryption', {
algorithm: 'm.megolm.v1.aes-sha2',
@ -37,9 +42,9 @@ function RoomEncryption({ roomId }) {
return (
<div className="room-encryption">
<SettingTile
title="Enable room encryption"
title={t("Molecules.RoomEncryption.enable_room_encryption")}
content={(
<Text variant="b3">Once enabled, encryption cannot be disabled.</Text>
<Text variant="b3">{t("Molecules.RoomEncryption.encryption_cannot_be_disabled")}</Text>
)}
options={(
<Toggle

View file

@ -8,6 +8,9 @@ import Text from '../../atoms/text/Text';
import RadioButton from '../../atoms/button/RadioButton';
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const visibility = {
WORLD_READABLE: 'world_readable',
SHARED: 'shared',
@ -17,19 +20,19 @@ const visibility = {
const items = [{
iconSrc: null,
text: 'Anyone (including guests)',
text: 'Molecules.RoomHistoryVisibility.world_readable',
type: visibility.WORLD_READABLE,
}, {
iconSrc: null,
text: 'Members (all messages)',
text: 'Molecules.RoomHistoryVisibility.shared',
type: visibility.SHARED,
}, {
iconSrc: null,
text: 'Members (messages after invite)',
text: 'Molecules.RoomHistoryVisibility.invited',
type: visibility.INVITED,
}, {
iconSrc: null,
text: 'Members (messages after join)',
text: 'Molecules.RoomHistoryVisibility.joined',
type: visibility.JOINED,
}];
@ -69,6 +72,8 @@ function RoomHistoryVisibility({ roomId }) {
const canChange = currentState.maySendStateEvent('m.room.history_visibility', userId);
const { t } = useTranslation();
return (
<div className="room-history-visibility">
{
@ -81,13 +86,13 @@ function RoomHistoryVisibility({ roomId }) {
disabled={(!canChange)}
>
<Text varient="b1">
<span>{item.text}</span>
<span>{t(item.text)}</span>
<RadioButton isActive={activeType === item.type} />
</Text>
</MenuItem>
))
}
<Text variant="b3">Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect.</Text>
<Text variant="b3">{t("Molecules.RoomHistoryVisibility.changes_only_affect_future")}</Text>
</div>
);
}

View file

@ -18,6 +18,9 @@ import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
import PeopleSelector from '../people-selector/PeopleSelector';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const PER_PAGE_MEMBER = 50;
function normalizeMembers(members) {
@ -113,6 +116,8 @@ function RoomMembers({ roomId }) {
const [members] = useMemberOfMembership(roomId, membership);
const [searchMembers, handleSearch] = useSearchMembers(members);
const { t } = useTranslation();
useEffect(() => {
setItemCount(PER_PAGE_MEMBER);
}, [searchMembers]);
@ -124,14 +129,14 @@ function RoomMembers({ roomId }) {
const mList = searchMembers ? searchMembers.data : members.slice(0, itemCount);
return (
<div className="room-members">
<MenuHeader>Search member</MenuHeader>
<MenuHeader>{t("Molecules.RoomMembers.search_title")}</MenuHeader>
<Input
onChange={handleSearch}
placeholder="Search for name"
placeholder={t("Molecules.RoomMembers.search_placeholder")}
autoFocus
/>
<div className="room-members__header">
<MenuHeader>{`${searchMembers ? `Found — ${mList.length}` : members.length} members`}</MenuHeader>
<MenuHeader>{t("Molecules.RoomMembers.found_members", {count: mList.length})}</MenuHeader>
<SegmentedControls
selected={
(() => {
@ -139,7 +144,7 @@ function RoomMembers({ roomId }) {
return getSegmentIndex[membership];
})()
}
segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]}
segments={[{ text: t("Molecules.RoomMembers.joined") }, { text: t("Molecules.RoomMembers.invited") }, { text: t("Molecules.RoomMembers.banned") }]}
onSelect={(index) => {
const memberships = ['join', 'invite', 'ban'];
setMembership(memberships[index]);
@ -162,7 +167,7 @@ function RoomMembers({ roomId }) {
&& (
<div className="room-members__status">
<Text variant="b2">
{searchMembers ? `No results found for "${searchMembers.term}"` : 'No members to display'}
{searchMembers ? t("Molecules.RoomMembers.invited", {term: searchMembers.term}) : t("Molecules.RoomMembers.no_members")}
</Text>
</div>
)
@ -171,7 +176,7 @@ function RoomMembers({ roomId }) {
mList.length !== 0
&& members.length > itemCount
&& searchMembers === null
&& <Button onClick={loadMorePeople}>View more</Button>
&& <Button onClick={loadMorePeople}>{t("Molecules.RoomMembers.view_more")}</Button>
}
</div>
</div>

View file

@ -14,21 +14,24 @@ import BellRingIC from '../../../../public/res/ic/outlined/bell-ring.svg';
import BellPingIC from '../../../../public/res/ic/outlined/bell-ping.svg';
import BellOffIC from '../../../../public/res/ic/outlined/bell-off.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const items = [{
iconSrc: BellIC,
text: 'Global',
text: 'Molecules.RoomNotification.default',
type: cons.notifs.DEFAULT,
}, {
iconSrc: BellRingIC,
text: 'All messages',
text: 'Molecules.RoomNotification.all_messages',
type: cons.notifs.ALL_MESSAGES,
}, {
iconSrc: BellPingIC,
text: 'Mentions & Keywords',
text: 'Molecules.RoomNotification.mentions_and_keywords',
type: cons.notifs.MENTIONS_AND_KEYWORDS,
}, {
iconSrc: BellOffIC,
text: 'Mute',
text: 'Molecules.RoomNotification.mute',
type: cons.notifs.MUTE,
}];
@ -114,6 +117,8 @@ function useNotifications(roomId) {
}
function RoomNotification({ roomId }) {
const { t } = useTranslation();
const [activeType, setNotification] = useNotifications(roomId);
return (
@ -127,7 +132,7 @@ function RoomNotification({ roomId }) {
onClick={() => setNotification(item)}
>
<Text varient="b1">
<span>{item.text}</span>
<span>{t(item.text)}</span>
<RadioButton isActive={activeType === item.type} />
</Text>
</MenuItem>

View file

@ -17,11 +17,16 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function RoomOptions({ roomId, afterOptionSelect }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const canInvite = room?.canInvite(mx.getUserId());
const { t } = useTranslation();
const handleMarkAsRead = () => {
markAsRead(roomId);
afterOptionSelect();
@ -34,9 +39,9 @@ function RoomOptions({ roomId, afterOptionSelect }) {
const handleLeaveClick = async () => {
afterOptionSelect();
const isConfirmed = await confirmDialog(
'Leave room',
`Are you sure that you want to leave "${room.name}" room?`,
'Leave',
t("Molecules.RoomOptions.leave.title"),
t("Molecules.RoomOptions.leave.subtitle", {room_name: room.name}),
t("Molecules.RoomOptions.leave.button_text"),
'danger',
);
if (!isConfirmed) return;
@ -45,17 +50,17 @@ function RoomOptions({ roomId, afterOptionSelect }) {
return (
<div style={{ maxWidth: '256px' }}>
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
<MenuHeader>{twemojify(t("Molecules.RoomOptions.title", {room_name: initMatrix.matrixClient.getRoom(roomId)?.name}))}</MenuHeader>
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>{t("Molecules.RoomOptions.mark_as_read")}</MenuItem>
<MenuItem
iconSrc={AddUserIC}
onClick={handleInviteClick}
disabled={!canInvite}
>
Invite
{t("Molecules.RoomOptions.invite")}
</MenuItem>
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>Leave</MenuItem>
<MenuHeader>Notification</MenuHeader>
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>{t("Molecules.RoomOptions.leave.button_text")}</MenuItem>
<MenuHeader>{t("Molecules.RoomOptions.notifications_heading")}</MenuHeader>
<RoomNotification roomId={roomId} />
</div>
);

View file

@ -17,142 +17,145 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
import { useForceUpdate } from '../../hooks/useForceUpdate';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const permissionsInfo = {
users_default: {
name: 'Default role',
description: 'Set default role for all members.',
name: 'Molecules.RoomPermissions.default_role.name',
description: 'Molecules.RoomPermissions.default_role.description',
default: 0,
},
events_default: {
name: 'Send messages',
description: 'Set minimum power level to send messages in room.',
name: 'Molecules.RoomPermissions.send_messages.name',
description: 'Molecules.RoomPermissions.send_messages.description',
default: 0,
},
'm.reaction': {
parent: 'events',
name: 'Send reactions',
description: 'Set minimum power level to send reactions in room.',
name: 'Molecules.RoomPermissions.reactions.name',
description: 'Molecules.RoomPermissions.reactions.description',
default: 0,
},
redact: {
name: 'Delete messages sent by others',
description: 'Set minimum power level to delete messages in room.',
name: 'Molecules.RoomPermissions.delete.name',
description: 'Molecules.RoomPermissions.delete.description',
default: 50,
},
notifications: {
name: 'Ping room',
description: 'Set minimum power level to ping room.',
name: 'Molecules.RoomPermissions.notifications.name',
description: 'Molecules.RoomPermissions.notifications.description',
default: {
room: 50,
},
},
'm.space.child': {
parent: 'events',
name: 'Manage rooms in space',
description: 'Set minimum power level to manage rooms in space.',
name: 'Molecules.RoomPermissions.manage_rooms.name',
description: 'Molecules.RoomPermissions.manage_rooms.description',
default: 50,
},
invite: {
name: 'Invite',
description: 'Set minimum power level to invite members.',
name: 'Molecules.RoomPermissions.invite.name',
description: 'Molecules.RoomPermissions.invite.description',
default: 50,
},
kick: {
name: 'Kick',
description: 'Set minimum power level to kick members.',
name: 'Molecules.RoomPermissions.kick.name',
description: 'Molecules.RoomPermissions.kick.description',
default: 50,
},
ban: {
name: 'Ban',
description: 'Set minimum power level to ban members.',
name: 'Molecules.RoomPermissions.ban.name',
description: 'Molecules.RoomPermissions.ban.description',
default: 50,
},
'm.room.avatar': {
parent: 'events',
name: 'Change avatar',
description: 'Set minimum power level to change room/space avatar.',
name: 'Molecules.RoomPermissions.change_avatar.name',
description: 'Molecules.RoomPermissions.change_avatar.description',
default: 50,
},
'm.room.name': {
parent: 'events',
name: 'Change name',
description: 'Set minimum power level to change room/space name.',
name: 'Molecules.RoomPermissions.change_name.name',
description: 'Molecules.RoomPermissions.change_name.description',
default: 50,
},
'm.room.topic': {
parent: 'events',
name: 'Change topic',
description: 'Set minimum power level to change room/space topic.',
name: 'Molecules.RoomPermissions.change_topic.name',
description: 'Molecules.RoomPermissions.change_topic.description',
default: 50,
},
state_default: {
name: 'Change settings',
description: 'Set minimum power level to change settings.',
name: 'Molecules.RoomPermissions.change_settings.name',
description: 'Molecules.RoomPermissions.change_settings.description',
default: 50,
},
'm.room.canonical_alias': {
parent: 'events',
name: 'Change published address',
description: 'Set minimum power level to publish and set main address.',
name: 'Molecules.RoomPermissions.change_published_address.name',
description: 'Molecules.RoomPermissions.change_published_address.description',
default: 50,
},
'm.room.power_levels': {
parent: 'events',
name: 'Change permissions',
description: 'Set minimum power level to change permissions.',
name: 'Molecules.RoomPermissions.change_permissions.name',
description: 'Molecules.RoomPermissions.change_permissions.description',
default: 50,
},
'm.room.encryption': {
parent: 'events',
name: 'Enable room encryption',
description: 'Set minimum power level to enable room encryption.',
name: 'Molecules.RoomPermissions.enable_room_encryption.name',
description: 'Molecules.RoomPermissions.enable_room_encryption.description',
default: 50,
},
'm.room.history_visibility': {
parent: 'events',
name: 'Change history visibility',
description: 'Set minimum power level to change room messages history visibility.',
name: 'Molecules.RoomPermissions.change_history_visibility.name',
description: 'Molecules.RoomPermissions.change_history_visibility.description',
default: 50,
},
'm.room.tombstone': {
parent: 'events',
name: 'Upgrade room',
description: 'Set minimum power level to upgrade room.',
name: 'Molecules.RoomPermissions.upgrade_room.name',
description: 'Molecules.RoomPermissions.upgrade_room.description',
default: 50,
},
'm.room.pinned_events': {
parent: 'events',
name: 'Pin messages',
description: 'Set minimum power level to pin messages in room.',
name: 'Molecules.RoomPermissions.pin_messages.name',
description: 'Molecules.RoomPermissions.pin_messages.description',
default: 50,
},
'm.room.server_acl': {
parent: 'events',
name: 'Change server ACLs',
description: 'Set minimum power level to change server ACLs.',
name: 'Molecules.RoomPermissions.change_acls.name',
description: 'Molecules.RoomPermissions.change_acls.description',
default: 50,
},
'im.vector.modular.widgets': {
parent: 'events',
name: 'Modify widgets',
description: 'Set minimum power level to modify room widgets.',
name: 'Molecules.RoomPermissions.modify_widgets.name',
description: 'Molecules.RoomPermissions.modify_widgets.description',
default: 50,
},
};
const roomPermsGroups = {
'General Permissions': ['users_default', 'events_default', 'm.reaction', 'redact', 'notifications'],
'Manage members permissions': ['invite', 'kick', 'ban'],
'Room profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels', 'm.room.encryption', 'm.room.history_visibility'],
'Other permissions': ['m.room.tombstone', 'm.room.pinned_events', 'm.room.server_acl', 'im.vector.modular.widgets'],
'Molecules.RoomPermissions.groups.general': ['users_default', 'events_default', 'm.reaction', 'redact', 'notifications'],
'Molecules.RoomPermissions.groups.manage_members': ['invite', 'kick', 'ban'],
'Molecules.RoomPermissions.groups.room': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
'Molecules.RoomPermissions.groups.settings': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels', 'm.room.encryption', 'm.room.history_visibility'],
'Molecules.RoomPermissions.groups.other': ['m.room.tombstone', 'm.room.pinned_events', 'm.room.server_acl', 'im.vector.modular.widgets'],
};
const spacePermsGroups = {
'General Permissions': ['users_default', 'm.space.child'],
'Manage members permissions': ['invite', 'kick', 'ban'],
'Space profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels'],
'Molecules.RoomPermissions.groups.general': ['users_default', 'm.space.child'],
'Molecules.RoomPermissions.groups.manage_members': ['invite', 'kick', 'ban'],
'Molecules.RoomPermissions.groups.space': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
'Molecules.RoomPermissions.groups.settings': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels'],
};
function useRoomStateUpdate(roomId) {
@ -181,6 +184,8 @@ function RoomPermissions({ roomId }) {
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
const { t } = useTranslation();
const handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
const handlePowerLevelChange = (newPowerLevel) => {
if (powerLevel === newPowerLevel) return;
@ -227,7 +232,7 @@ function RoomPermissions({ roomId }) {
const groupedPermKeys = permsGroups[groupKey];
return (
<div className="room-permissions__card" key={groupKey}>
<MenuHeader>{groupKey}</MenuHeader>
<MenuHeader>{t(groupKey)}</MenuHeader>
{
groupedPermKeys.map((permKey) => {
const permInfo = permissionsInfo[permKey];
@ -247,8 +252,8 @@ function RoomPermissions({ roomId }) {
return (
<SettingTile
key={permKey}
title={permInfo.name}
content={<Text variant="b3">{permInfo.description}</Text>}
title={t(permInfo.name)}
content={<Text variant="b3">{t(permInfo.description)}</Text>}
options={(
<Button
onClick={

View file

@ -21,6 +21,9 @@ import { useStore } from '../../hooks/useStore';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function RoomProfile({ roomId }) {
const isMountStore = useStore();
const [isEditing, setIsEditing] = useState(false);
@ -30,6 +33,8 @@ function RoomProfile({ roomId }) {
type: cons.status.PRE_FLIGHT,
});
const { t } = useTranslation();
const mx = initMatrix.matrixClient;
const isDM = initMatrix.roomList.directs.has(roomId);
let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
@ -76,7 +81,7 @@ function RoomProfile({ roomId }) {
const newName = roomNameInput.value;
if (newName !== roomName && roomName.trim() !== '') {
setStatus({
msg: 'Saving room name...',
msg: t("Molecules.RoomProfile.saving_room_name"),
type: cons.status.IN_FLIGHT,
});
await mx.setRoomName(roomId, newName);
@ -87,7 +92,7 @@ function RoomProfile({ roomId }) {
if (newTopic !== roomTopic) {
if (isMountStore.getItem()) {
setStatus({
msg: 'Saving room topic...',
msg: t("Molecules.RoomProfile.saving_room_topic"),
type: cons.status.IN_FLIGHT,
});
}
@ -96,13 +101,13 @@ function RoomProfile({ roomId }) {
}
if (!isMountStore.getItem()) return;
setStatus({
msg: 'Saved successfully',
msg: t("Molecules.RoomProfile.save_success"),
type: cons.status.SUCCESS,
});
} catch (err) {
if (!isMountStore.getItem()) return;
setStatus({
msg: err.message || 'Unable to save.',
msg: err.message || t("Molecules.RoomProfile.save_failed"),
type: cons.status.ERROR,
});
}
@ -119,9 +124,9 @@ function RoomProfile({ roomId }) {
const handleAvatarUpload = async (url) => {
if (url === null) {
const isConfirmed = await confirmDialog(
'Remove avatar',
'Are you sure that you want to remove room avatar?',
'Remove',
t("Molecules.RoomProfile.remove_avatar_title"),
t("Molecules.RoomProfile.remove_avatar_subtitle"),
t("Molecules.RoomProfile.remove_avatar_button"),
'caution',
);
if (isConfirmed) {
@ -132,16 +137,20 @@ function RoomProfile({ roomId }) {
const renderEditNameAndTopic = () => (
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" required />}
{canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />}
{(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}</Text>}
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label={t("Molecules.RoomProfile.name_label")} required />}
{canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label={t("Molecules.RoomProfile.topic_label")} />}
{(!canChangeName || !canChangeTopic) && <Text variant="b3">{
room.isSpaceRoom() ?
canChangeName ? "Molecules.RoomProfile.permission_change_space_name" : "Molecules.RoomProfile.permission_change_space_topic" :
canChangeName ? "Molecules.RoomProfile.permission_change_room_name": "Molecules.RoomProfile.permission_change_room_topic"
}</Text>}
{ status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
{ status.type === cons.status.SUCCESS && <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">{status.msg}</Text>}
{ status.type === cons.status.ERROR && <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">{status.msg}</Text>}
{ status.type !== cons.status.IN_FLIGHT && (
<div>
<Button type="submit" variant="primary">Save</Button>
<Button onClick={handleCancelEditing}>Cancel</Button>
<Button type="submit" variant="primary">{t("common.save")}</Button>
<Button onClick={handleCancelEditing}>{t("common.cancel")}</Button>
</div>
)}
</form>
@ -155,7 +164,7 @@ function RoomProfile({ roomId }) {
<IconButton
src={PencilIC}
size="extra-small"
tooltip="Edit"
tooltip={t("common.edit")}
onClick={() => setIsEditing(true)}
/>
)}

View file

@ -20,6 +20,9 @@ import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const roomIdToBackup = new Map();
function useRoomSearch(roomId) {
@ -117,6 +120,8 @@ function RoomSearch({ roomId }) {
search(term);
};
const { t } = useTranslation();
const renderTimeline = (timeline) => (
<div className="room-search__result-item" key={timeline[0].getId()}>
{ timeline.map((mEvent) => {
@ -139,37 +144,37 @@ function RoomSearch({ roomId }) {
return (
<div className="room-search">
<form className="room-search__form" onSubmit={handleSearch}>
<MenuHeader>Room search</MenuHeader>
<MenuHeader>{t("Molecules.RoomSearch.title")}</MenuHeader>
<div>
<Input
placeholder="Search for keywords"
placeholder={t("Molecules.RoomSearch.placeholder")}
name="room-search-input"
disabled={isRoomEncrypted}
autoFocus
/>
<Button iconSrc={SearchIC} variant="primary" type="submit">Search</Button>
<Button iconSrc={SearchIC} variant="primary" type="submit">{t("Molecules.RoomSearch.search_button")}</Button>
</div>
{searchData?.results.length > 0 && (
<Text>{`${searchData.count} results for "${searchTerm}"`}</Text>
<Text>{t("Molecules.RoomSearch.results", {count: searchData.count, term: searchTerm})}</Text>
)}
{!isRoomEncrypted && searchData === null && (
<div className="room-search__help">
{status.type === cons.status.IN_FLIGHT && <Spinner />}
{status.type === cons.status.IN_FLIGHT && <Text>Searching room messages...</Text>}
{status.type === cons.status.IN_FLIGHT && <Text>{t("Molecules.RoomSearch.searching")}</Text>}
{status.type === cons.status.PRE_FLIGHT && <RawIcon src={SearchIC} size="large" />}
{status.type === cons.status.PRE_FLIGHT && <Text>Search room messages</Text>}
{status.type === cons.status.ERROR && <Text>Failed to search messages</Text>}
{status.type === cons.status.PRE_FLIGHT && <Text>{t("Molecules.RoomSearch.subtitle")}</Text>}
{status.type === cons.status.ERROR && <Text>{t("Molecules.RoomSearch.failed")}</Text>}
</div>
)}
{!isRoomEncrypted && searchData?.results.length === 0 && (
<div className="room-search__help">
<Text>No results found</Text>
<Text>{t("Molecules.RoomSearch.no_results")}</Text>
</div>
)}
{isRoomEncrypted && (
<div className="room-search__help">
<Text>Search does not work in encrypted room</Text>
<Text>{t("Molecules.RoomSearch.encrypted_room")}</Text>
</div>
)}
</form>
@ -184,7 +189,7 @@ function RoomSearch({ roomId }) {
{searchData?.next_batch && (
<div className="room-search__more">
{status.type !== cons.status.IN_FLIGHT && (
<Button onClick={paginate}>Load more</Button>
<Button onClick={paginate}>{t("Molecules.RoomSearch.load_more")}</Button>
)}
{status.type === cons.status.IN_FLIGHT && <Spinner />}
</div>

View file

@ -8,6 +8,7 @@ import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
import { t } from 'i18next';
function RoomTile({
avatarSrc, name, id,
@ -27,8 +28,8 @@ function RoomTile({
<Text variant="b3">
{
inviterName !== null
? `Invited by ${inviterName} to ${id}${memberCount === null ? '' : `${memberCount} members`}`
: id + (memberCount === null ? '' : `${memberCount} members`)
? t("Molecules.RoomTile.invited_by_user", {inviter: inviterName, count: memberCount || 0, id: id})
: t("Molecules.RoomTile.invited", {count: memberCount || 0, id: id})
}
</Text>
{

View file

@ -15,6 +15,9 @@ import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const visibility = {
INVITE: 'invite',
RESTRICTED: 'restricted',
@ -75,19 +78,21 @@ function RoomVisibility({ roomId }) {
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
const { t } = useTranslation();
const items = [{
iconSrc: isSpace ? SpaceLockIC : HashLockIC,
text: 'Private (invite only)',
text: 'Molecules.RoomVisibility.private',
type: visibility.INVITE,
unsupported: false,
}, {
iconSrc: isSpace ? SpaceIC : HashIC,
text: roomVersion < 8 ? 'Restricted (unsupported: required room upgrade)' : 'Restricted (space member can join)',
text: roomVersion < 8 ? 'Molecules.RoomVisibility.restricted_unsupported' : 'Molecules.RoomVisibility.restricted',
type: visibility.RESTRICTED,
unsupported: roomVersion < 8 || noSpaceParent,
}, {
iconSrc: isSpace ? SpaceGlobeIC : HashGlobeIC,
text: 'Public (anyone can join)',
text: 'Molecules.RoomVisibility.public',
type: visibility.PUBLIC,
unsupported: false,
}];
@ -104,7 +109,7 @@ function RoomVisibility({ roomId }) {
disabled={(!canChange || item.unsupported)}
>
<Text varient="b1">
<span>{item.text}</span>
<span>{t(item.text)}</span>
<RadioButton isActive={activeType === item.type} />
</Text>
</MenuItem>

View file

@ -25,6 +25,10 @@ import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function SpaceAddExistingContent({ roomId }) {
const mountStore = useStore(roomId);
const [debounce] = useState(new Debounce());
@ -37,6 +41,9 @@ function SpaceAddExistingContent({ roomId }) {
spaces, rooms, directs, roomIdToParents,
} = initMatrix.roomList;
const { t } = useTranslation();
useEffect(() => {
const allIds = [...spaces, ...rooms, ...directs].filter((rId) => (
rId !== roomId && !roomIdToParents.get(rId)?.has(roomId)
@ -59,7 +66,7 @@ function SpaceAddExistingContent({ roomId }) {
};
const handleAdd = async () => {
setProcess(`Adding ${selected.length} items...`);
setProcess(t("Molecules.SpaceAddExisting.adding_items", {count: selected.length}));
const promises = selected.map((rId) => {
const room = mx.getRoom(rId);
@ -119,12 +126,12 @@ function SpaceAddExistingContent({ roomId }) {
<Input
name="searchInput"
onChange={handleSearch}
placeholder="Search room"
placeholder={t("Molecules.SpaceAddExisting.search_rooms_placeholder")}
autoFocus
/>
<IconButton size="small" type="button" onClick={handleSearchClear} src={CrossIC} />
</form>
{searchIds?.length === 0 && <Text>No results found</Text>}
{searchIds?.length === 0 && <Text>{t("Molecules.SpaceAddExisting.no_results")}</Text>}
{
(searchIds || allRoomIds).map((rId) => {
const room = mx.getRoom(rId);
@ -171,9 +178,9 @@ function SpaceAddExistingContent({ roomId }) {
{selected.length !== 0 && (
<div className="space-add-existing__footer">
{process && <Spinner size="small" />}
<Text weight="medium">{process || `${selected.length} item selected`}</Text>
<Text weight="medium">{t("Molecules.SpaceAddExisting.items_selected", {count: selected.length})}</Text>
{ !process && (
<Button onClick={handleAdd} variant="primary">Add</Button>
<Button onClick={handleAdd} variant="primary">{t("Molecules.SpaceAddExisting.add_button")}</Button>
)}
</div>
)}
@ -212,10 +219,10 @@ function SpaceAddExisting() {
title={(
<Text variant="s1" weight="medium" primary>
{roomId && twemojify(room.name)}
<span style={{ color: 'var(--tc-surface-low)' }}> add existing rooms</span>
<span style={{ color: 'var(--tc-surface-low)' }}> {t("Molecules.SpaceAddExisting.subtitle")}</span>
</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip={t("common.close")} />}
onRequestClose={requestClose}
>
{

View file

@ -26,6 +26,9 @@ import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function SpaceOptions({ roomId, afterOptionSelect }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
@ -33,6 +36,8 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
const isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId);
const { t } = useTranslation();
const handleInviteClick = () => {
openInviteUser(roomId);
afterOptionSelect();
@ -59,9 +64,9 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
const handleLeaveClick = async () => {
afterOptionSelect();
const isConfirmed = await confirmDialog(
'Leave space',
`Are you sure that you want to leave "${room.name}" space?`,
'Leave',
t("Molecules.SpaceOptions.leave_space"),
t("Molecules.SpaceOptions.leave_space_confirmation", {space: room.name}),
t("Molecules.SpaceOptions.leave_space_confirmation"),
'danger',
);
if (!isConfirmed) return;
@ -75,29 +80,29 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
onClick={handleCategorizeClick}
iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
>
{isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'}
{isCategorized ? t("Organisms.SpaceSettings.uncategorize_subspaces") : t("Organisms.SpaceSettings.categorize_subspaces")}
</MenuItem>
<MenuItem
onClick={handlePinClick}
iconSrc={isPinned ? PinFilledIC : PinIC}
>
{isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'}
{isPinned ? t("Organisms.SpaceSettings.unpin_sidebar") : t("Organisms.SpaceSettings.pin_sidebar")}
</MenuItem>
<MenuItem
iconSrc={AddUserIC}
onClick={handleInviteClick}
disabled={!canInvite}
>
Invite
{t("Molecules.SpaceOptions.invite")}
</MenuItem>
<MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>Manage rooms</MenuItem>
<MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>Settings</MenuItem>
<MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>{t("Molecules.SpaceOptions.manage_rooms")}</MenuItem>
<MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>{t("Molecules.SpaceOptions.settings")}</MenuItem>
<MenuItem
variant="danger"
onClick={handleLeaveClick}
iconSrc={LeaveArrowIC}
>
Leave
{t("Molecules.SpaceOptions.leave")}
</MenuItem>
</div>
);

View file

@ -6,8 +6,12 @@ import { createTemporaryClient, startSsoLogin } from '../../../client/action/aut
import Button from '../../atoms/button/Button';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function SSOButtons({ type, identityProviders, baseUrl }) {
const tempClient = createTemporaryClient(baseUrl);
const { t } = useTranslation();
function handleClick(id) {
startSsoLogin(baseUrl, type, id);
}
@ -24,7 +28,7 @@ function SSOButtons({ type, identityProviders, baseUrl }) {
<button key={idp.id} type="button" className="sso-btn" onClick={() => handleClick(idp.id)}>
<img className="sso-btn__img" src={tempClient.mxcUrlToHttp(idp.icon)} alt={idp.name} />
</button>
) : <Button key={idp.id} className="sso-btn__text-only" onClick={() => handleClick(idp.id)}>{`Login with ${idp.name}`}</Button>
) : <Button key={idp.id} className="sso-btn__text-only" onClick={() => handleClick(idp.id)}>{t("Molecules.SSOButtons.login_with", {idp_name: idp.name})}</Button>
))}
</div>
);

View file

@ -18,6 +18,9 @@ import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg
import { useForceUpdate } from '../../hooks/useForceUpdate';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function Selector({
roomId, isDM, drawerPostie, onClick,
}) {
@ -30,6 +33,8 @@ function Selector({
const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE;
const { t } = useTranslation();
const [, forceUpdate] = useForceUpdate();
useEffect(() => {
@ -69,7 +74,7 @@ function Selector({
options={(
<IconButton
size="extra-small"
tooltip="Options"
tooltip={t("common.options")}
tooltipPlacement="right"
src={VerticalMenuIC}
onClick={openOptions}

View file

@ -25,6 +25,9 @@ import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function simplyfiMembers(members) {
const mx = initMatrix.matrixClient;
return members.map((member) => ({
@ -50,6 +53,8 @@ function PeopleDrawer({ roomId }) {
const [searchedMembers, setSearchedMembers] = useState(null);
const searchRef = useRef(null);
const { t } = useTranslation();
const getMembersWithMembership = useCallback(
(mship) => room.getMembersWithMembership(mship),
[roomId, membership],
@ -129,11 +134,11 @@ function PeopleDrawer({ roomId }) {
<Header>
<TitleWrapper>
<Text variant="s1" primary>
People
<Text className="people-drawer__member-count" variant="b3">{`${room.getJoinedMemberCount()} members`}</Text>
{t("Organisms.PeopleDrawer.title")}
<Text className="people-drawer__member-count" variant="b3">{t("Organisms.PeopleDrawer.members", {count: room.getJoinedMemberCount()})}</Text>
</Text>
</TitleWrapper>
<IconButton onClick={() => openInviteUser(roomId)} tooltip="Invite" src={AddUserIC} disabled={!canInvite} />
<IconButton onClick={() => openInviteUser(roomId)} tooltip={t("Organisms.PeopleDrawer.invite_tooltip")} src={AddUserIC} disabled={!canInvite} />
</Header>
<div className="people-drawer__content-wrapper">
<div className="people-drawer__scrollable">
@ -150,7 +155,7 @@ function PeopleDrawer({ roomId }) {
return getSegmentIndex[membership];
})()
}
segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]}
segments={[{ text: t("Organisms.PeopleDrawer.joined")}, { text: t("Organisms.PeopleDrawer.invited") }, { text: t("Organisms.PeopleDrawer.banned") }]}
onSelect={(index) => {
const selectSegment = [
() => setMembership('join'),
@ -176,7 +181,7 @@ function PeopleDrawer({ roomId }) {
(searchedMembers?.data.length === 0 || memberList.length === 0)
&& (
<div className="people-drawer__noresult">
<Text variant="b2">No results found!</Text>
<Text variant="b2">{t("Organisms.PeopleDrawer.search_no_results")}</Text>
</div>
)
}
@ -186,7 +191,7 @@ function PeopleDrawer({ roomId }) {
&& memberList.length > itemCount
&& searchedMembers === null
&& (
<Button onClick={loadMorePeople}>View more</Button>
<Button onClick={loadMorePeople}>{t("Organisms.PeopleDrawer.view_more")}</Button>
)
}
</div>
@ -196,7 +201,7 @@ function PeopleDrawer({ roomId }) {
<div className="people-drawer__sticky">
<form onSubmit={(e) => e.preventDefault()} className="people-search">
<RawIcon size="small" src={SearchIC} />
<Input forwardRef={searchRef} type="text" onChange={handleSearch} placeholder="Search" required />
<Input forwardRef={searchRef} type="text" onChange={handleSearch} placeholder={t("Organisms.PeopleDrawer.placeholder")} required />
{
searchedMembers !== null
&& <IconButton onClick={handleSearch} size="small" src={CrossIC} />

View file

@ -104,7 +104,6 @@ function RoomViewFloating({
const room = initMatrix.matrixClient.getRoom(roomId)
const getUserDisplayName = (userId) => {
console.log(userId);
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
return getUsername(userId);
};
@ -135,7 +134,13 @@ function RoomViewFloating({
<Text variant="b2">
<Trans
i18nKey="Organisms.RoomViewFloating.user_typing"
values={{count: typingMembers.size, user_one: getUserDisplayName(typingMemberValues?.[0]), user_two: getUserDisplayName(typingMemberValues?.[1]), user_three: getUserDisplayName(typingMemberValues?.[2]), user_four: getUserDisplayName(typingMemberValues?.[3])}}
values={{
count: typingMembers.size,
user_one: twemojify(getUserDisplayName(typingMemberValues?.[0])),
user_two: twemojify(getUserDisplayName(typingMemberValues?.[1])),
user_three: twemojify(getUserDisplayName(typingMemberValues?.[2])),
user_four: twemojify(getUserDisplayName(typingMemberValues?.[3]))
}}
components={{bold: <b/>}}
/>
</Text>

View file

@ -39,6 +39,11 @@ export function getUsernameOfRoomMember(roomMember) {
return roomMember.name || roomMember.userId;
}
export function getUserDisplayName(room, userId){
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
return getUsername(userId);
}
export async function isRoomAliasAvailable(alias) {
try {
const result = await initMatrix.matrixClient.resolveRoomAlias(alias);