This commit is contained in:
OfficialDakari 2024-08-01 23:21:26 +05:00
parent bda2961276
commit 864d792da6
24 changed files with 2757 additions and 0 deletions

18
find.js Normal file
View file

@ -0,0 +1,18 @@
import fs from 'fs';
const files = fs.readdirSync('src/', {
recursive: true
});
const q = process.argv[2];
for (const f of files) {
try {
const b = fs.readFileSync(`src/${f}`, 'utf-8');
if (b.includes(q) || f.includes(q)) {
console.log(f);
}
} catch (error) {
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

107
public/worker.js Normal file
View file

@ -0,0 +1,107 @@
function fetchFromIndexedDB() {
return new Promise((resolve, reject) => {
const dbRequest = indexedDB.open("CinnyDB", 1);
dbRequest.onsuccess = function (event) {
const db = event.target.result;
const transaction = db.transaction("tokens", "readonly");
const store = transaction.objectStore("tokens");
const getRequest = store.get(1);
getRequest.onsuccess = function () {
if (getRequest.result) {
resolve(getRequest.result);
} else {
reject(new Error("No data found"));
}
};
getRequest.onerror = function (error) {
reject(error);
};
};
dbRequest.onerror = function (error) {
reject(error);
};
});
}
async function isClientOpen() {
const clientList = await clients.matchAll({
type: 'window',
includeUncontrolled: true // включает неуправляемые воркером вкладки
});
return clientList.length > 0;
}
self.addEventListener("push", async (event) => {
const { baseUrl, accessToken } = await fetchFromIndexedDB();
if (typeof baseUrl !== 'string' || typeof accessToken !== 'string') return;
if (await isClientOpen()) return;
var { prio, event_id, room_id, sender_display_name, sender, content, room_name, type } = JSON.parse(event.data.text());
if (!type || !content || !sender) {
const eventResponse = await fetch(`${baseUrl}/_matrix/client/v3/rooms/${room_id}/event/${event_id}`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!eventResponse.ok) return;
const c = await eventResponse.json();
content = c.content;
type = c.type;
sender = c.sender;
}
if (type !== 'm.room.message' && type !== 'm.room.encrypted') return;
var senderName = sender_display_name ?? sender;
if (!sender_display_name) {
try {
const profileResponse = await fetch(`${baseUrl}/_matrix/client/r0/profile/${sender}`);
if (profileResponse.ok) {
const body = await profileResponse.json();
if (typeof body.displayname === 'string') {
senderName = body.displayname;
}
}
} catch (error) {
}
}
const notifications = await self.registration.getNotifications({ includeTriggered: true });
const targetNotification = notifications.find(n => n.data && n.data.event_id == event_id && n.data.room_id == room_id);
if (targetNotification) return;
const name = typeof room_name === 'string' ? room_name : room_id;
var body = `New ${type == 'm.room.encrypted' ? 'encrypted message' : 'message'}`;
if (['m.text', 'm.notice'].includes(content?.msgtype)) {
body = content.body;
}
self.registration.showNotification(`${senderName}${name ? ` @ ${name}` : ''}`, {
body,
data: {
event_id,
room_id
}
});
});
self.addEventListener('message', async (event) => {
const { action, room_id } = event.data;
if (action === 'closeNotification') {
const notifications = await self.registration.getNotifications({ includeTriggered: true });
const targetNotifications = notifications.filter(n => n.data && n.data.room_id == room_id);
for (const notification of targetNotifications) {
notification.close();
}
}
});
self.addEventListener('notificationclick', async (event) => {
clients.openWindow('/#/');
});

View file

@ -0,0 +1,43 @@
.ql-container>.ql-editor.ql-blank::before {
color: var(--tc-surface-low);
font-family: inherit;
font-size: inherit;
font-style: inherit;
}
.ql-container,
.ql-editor {
font-family: inherit;
font-size: inherit;
font-style: inherit;
}
.ql-container.ql-snow {
border: none !important;
}
// .iql-mention {
// vertical-align: middle;
// height: 20px;
// }
.iql-mention {
background-color: #222222;
color: rgb(211, 211, 211);
border-radius: 3px;
padding: 2px 5px;
margin: 2px;
display: inline;
/* Обеспечивает корректное поведение курсора */
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
pointer-events: none;
}
// .user-mention:empty::before, .user-mention:empty::after {
// content: "";
// display: inline-block;
// }

1
src/app/components/editor/quill.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module 'quill-markdown-shortcuts';

View file

@ -0,0 +1,132 @@
import { Box, Icon, Icons, MenuItem, Text, as, color, config } from 'folds';
import React, { useState, useEffect, useCallback } from 'react';
import * as css from './style.css';
import ProgressBar from '../../progressbar';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { MatrixClient, MatrixEvent } from 'matrix-js-sdk';
type PollAnswerProps = {
id: string,
text: string,
roomId?: string,
eventId?: string,
votes?: number,
updatePoll?: () => void,
disabled?: boolean,
disclose?: boolean
};
function vote({ id, text, roomId, eventId }: PollAnswerProps, mx: MatrixClient, updatePoll: () => void) {
if (!roomId || !eventId) return;
mx.sendEvent(roomId, 'org.matrix.msc3381.poll.response', {
body: id,
'm.relates_to': {
rel_type: 'm.reference',
event_id: eventId
},
'org.matrix.msc3381.poll.response': {
answers: [
id
]
}
}).then(updatePoll);
}
export const PollAnswer = as<'div', PollAnswerProps>(
(
{ id, text, roomId, eventId, votes, updatePoll, disabled, disclose },
ref
) => {
const mx = useMatrixClient();
useEffect(() => {
if (updatePoll) updatePoll();
}, [updatePoll]);
return <MenuItem
radii="300"
size='400'
onClick={() => vote({ id, text, roomId, eventId }, mx, updatePoll ?? (() => null))}
disabled={disabled}
>
<Text
className={css.PollAnswerItemText}
as="span"
>
{text}
</Text>
{
disclose ? (
<Text
className={css.PollAnswerItemVoted}
as="span"
>
{votes}
</Text>
) : (
<></>
)
}
</MenuItem>;
}
);
type PollContentProps = {
content: Record<string, any>,
event: MatrixEvent
};
export const PollContent = as<'div', PollContentProps>((
{ content, event },
ref
) => {
const mx = useMatrixClient();
const [answers, setAnswers] = useState(content['org.matrix.msc3381.poll.start'].answers);
const [thisUserVoted, setThisUserVoted] = useState(['']);
const [closed, setClosed] = useState(0);
const updateVotes = useCallback(() => {
const roomId = event.getRoomId();
const eventId = event.getId();
const userId = mx.getUserId();
if (event && roomId && eventId && userId) {
mx.relations(roomId, eventId, 'm.reference', 'org.matrix.msc3381.poll.end').then(({ events: closeEvents }) => {
const ev = closeEvents.find(x => x.sender?.userId == event.sender?.userId);
if (ev) {
setClosed(ev.localTimestamp);
}
});
mx.relations(roomId, eventId, 'm.reference', 'org.matrix.msc3381.poll.response').then(({ events }) => {
const votes: Record<string, string[]> = {};
for (const ev of events.sort((a, b) => a.localTimestamp - b.localTimestamp).filter(x => closed == 0 || x.localTimestamp < closed)) {
const c = ev.getContent();
if (typeof c['org.matrix.msc3381.poll.response'] === 'object' && typeof c['org.matrix.msc3381.poll.response'].answers === 'object') {
if (ev.sender) {
votes[ev.sender.userId] = c['org.matrix.msc3381.poll.response'].answers;
}
}
}
setAnswers(content['org.matrix.msc3381.poll.start'].answers.map((answer: { id: string, 'org.matrix.msc1767.text': string }) => {
const v = Object.values(votes).filter(a => a.includes(answer.id)).length;
return { ...answer, votes: v };
}));
setThisUserVoted(votes[userId] ?? []);
});
}
}, [content, event, mx]);
useEffect(() => {
updateVotes();
}, [updateVotes]);
return <Box as="span" direction='Column' alignItems="Start" gap="100" ref={ref}>
<b>Poll: {content['org.matrix.msc3381.poll.start'].question['org.matrix.msc1767.text']}</b>
<Box direction="Column" gap="100" className={css.PollAnswers}>
{answers.map(
(answer: { id: string, 'org.matrix.msc1767.text': string, votes: number }) => (
<PollAnswer disclose={closed != 0 || content['org.matrix.msc3381.poll.start'].kind == 'org.matrix.msc3381.disclosed'} disabled={closed != 0 || thisUserVoted.includes(answer.id)} key={answer.id} id={answer.id} text={answer['org.matrix.msc1767.text']} updatePoll={updateVotes} votes={answer.votes} roomId={event.getRoomId()} eventId={event.getId()} />
)
)}
</Box>
</Box>;
});

View file

@ -0,0 +1,36 @@
import React from 'react';
const ProgressBar = (props: { bgcolor: string, completed: number }) => {
const { bgcolor, completed } = props;
const containerStyles = {
height: 20,
width: '100%',
backgroundColor: "#e0e0de",
borderRadius: '2px',
position: 'relative'
};
const fillerStyles = {
height: '100%',
width: `${completed}%`,
backgroundColor: bgcolor,
borderRadius: 'inherit',
textAlign: 'right'
};
const labelStyles = {
padding: 5,
color: 'white',
fontWeight: 'bold'
};
return (
<div style={containerStyles}>
<div style={fillerStyles}>
<span style={labelStyles}>{`${completed}%`}</span>
</div>
</div>
);
};
export default ProgressBar;

View file

@ -0,0 +1,19 @@
import { useEffect } from "react";
export const useBackButton = (callback) => {
useEffect(() => {
// Add a fake history event so that the back button does nothing if pressed once
window.history.pushState('fake-route', document.title, window.location.href);
addEventListener('popstate', callback);
// Here is the cleanup when this component unmounts
return () => {
removeEventListener('popstate', callback);
// If we left without using the back button, aka by using a button on the page, we need to clear out that fake history event
if (window.history.state === 'fake-route') {
window.history.back();
}
};
}, []);
};

View file

@ -0,0 +1,41 @@
import { useCallback } from "react";
import { useMatrixClient } from "./useMatrixClient";
import { UserEvent } from "matrix-js-sdk";
type PresenceData = {
lastActiveAgo: number;
lastPresenceTs?: number;
presence: 'online' | 'offline' | 'unavailable';
presenceStatusMsg?: string;
};
export const usePresences = () => {
const mx = useMatrixClient();
const presences: Record<string, PresenceData> = {};
const getPresence = useCallback(
async (userId: string) => {
if (presences[userId]) {
return presences[userId];
}
const presence = await mx.getPresence(userId);
return {
lastActiveAgo: presence.last_active_ago,
presence: presence.presence,
presenceStatusMsg: presence.status_msg
}
},
[mx, presences]
);
mx.on(UserEvent.Presence, (ev, user) => {
presences[user.userId] = {
lastActiveAgo: user.lastActiveAgo,
lastPresenceTs: user.lastPresenceTs,
presence: user.presence,
presenceStatusMsg: user.presenceStatusMsg
};
});
return getPresence;
};

View file

@ -0,0 +1,67 @@
import { TouchEvent, useState } from "react";
export const useSwipeLeft = (handleReplyId: (replyId: string | null) => void) => {
// States used for swipe-left-reply. Used for animations and determining whether we should reply or not.
const [isTouchingSide, setTouchingSide] = useState(false);
const [sideMoved, setSideMoved] = useState(0);
const [sideMovedInit, setSideMovedInit] = useState(0);
const [swipingId, setSwipingId] = useState("");
// Touch handlers for the Message components. If touch starts at 90% of the right, it will trigger the swipe-left-reply.
let lastTouch = 0, sideVelocity = 0;
function onTouchStart(event: TouchEvent, replyId: string | undefined) {
if (event.touches.length != 1) return setTouchingSide(false);
if (
event.touches[0].clientX > window.innerWidth * 0.7 &&
!Array.from(document.elementsFromPoint(event.touches[0].clientX, event.touches[0].clientY)[0].classList).some(c => c.startsWith("ImageViewer")) // Disable gesture if ImageViewer is up. There's probably a better way I don't know
) {
setTouchingSide(true);
setSideMoved(event.touches[0].clientX);
setSideMovedInit(event.touches[0].clientX);
setSwipingId(replyId || "");
lastTouch = Date.now();
}
}
function onTouchEnd(event: TouchEvent) {
if (isTouchingSide) {
if (sideMoved) {
setSideMovedInit(sideMovedInit => {
// || sideVelocity <= -(window.innerWidth * 0.05 / 250)
if ((sideMoved - sideMovedInit) < -(window.innerWidth * 0.15)) setSwipingId(swipingId => {
event.preventDefault();
setTimeout(() => handleReplyId(swipingId), 100);
return "";
});
return 0;
});
}
setSideMoved(0);
}
setTouchingSide(false);
}
function onTouchMove(event: TouchEvent, replyId: string | undefined) {
if (event.touches.length != 1) return;
if (isTouchingSide) {
event.preventDefault();
if (swipingId == replyId) {
if (event.changedTouches.length != 1) setSideMoved(0);
else setSideMoved(sideMoved => {
const newSideMoved = event.changedTouches[0].clientX;
//sideVelocity = (newSideMoved - sideMoved); // / (Date.now() - lastTouch);
//lastTouch = Date.now();
return newSideMoved;
});
}
}
}
return {
isTouchingSide,
sideMoved,
sideMovedInit,
swipingId,
onTouchStart,
onTouchMove,
onTouchEnd
}
}

View file

@ -0,0 +1,63 @@
import { RefObject, TouchEvent, useState } from "react"
import { openNavigation } from "../../client/action/navigation";
export const useTouchMenu = (navWrapperRef: RefObject<HTMLDivElement>, classNameSided: string) => {
const [lastTouch, setLastTouch] = useState(0);
const [sideVelocity, setSideVelocity] = useState(0);
const [sideMoved, setSideMoved] = useState(0);
const [isTouchingSide, setTouchingSide] = useState(false);
// Touch handlers for window object. If the touch starts at 10% of the left of the screen, it will trigger the swipe-right-menu.
const onTouchStart = (event: TouchEvent) => {
if (!navWrapperRef.current?.classList.contains(classNameSided)) return;
if (event.touches.length != 1) return setTouchingSide(false);
if (event.touches[0].clientX < window.innerWidth * 0.1) {
setTouchingSide(true);
setLastTouch(Date.now());
}
}
const onTouchEnd = (event: TouchEvent) => {
if (!navWrapperRef.current?.classList.contains(classNameSided)) return;
setTouchingSide(isTouchingSide => {
if (isTouchingSide) {
setSideMoved(sideMoved => {
if (sideMoved) {
event.preventDefault();
if (sideMoved > window.innerWidth * 0.5 || sideVelocity >= (window.innerWidth * 0.1 / 250)) openNavigation();
}
setLastTouch(0);
setSideVelocity(0);
return 0;
});
}
return false;
});
}
const onTouchMove = (event: TouchEvent) => {
if (!navWrapperRef.current?.classList.contains(classNameSided)) return;
setTouchingSide(isTouchingSide => {
if (isTouchingSide) {
event.preventDefault();
if (event.changedTouches.length != 1) {
setSideMoved(0);
return false;
}
setSideMoved(sideMoved => {
const newSideMoved = event.changedTouches[0].clientX;
setSideVelocity((newSideMoved - sideMoved) / (Date.now() - lastTouch));
setLastTouch(Date.now());
return newSideMoved;
});
}
return isTouchingSide;
});
}
return {
isTouchingSide,
sideMoved,
onTouchStart,
onTouchMove,
onTouchEnd
}
}

View file

@ -0,0 +1,133 @@
import { useAtomValue } from "jotai";
import { useBackButton } from "../../hooks/useBackButton";
import { isHidden, useDirects, useRooms } from "../../state/hooks/roomList";
import { mDirectAtom } from "../../state/mDirectList";
import { allRoomsAtom } from "../../state/room-list/roomList";
import { useEffect, useState } from "react";
import { useRoomNavigate } from "../../hooks/useRoomNavigate";
import initMatrix from "../../../client/initMatrix";
import RawModal from "../../atoms/modal/RawModal";
import ScrollView from "../../atoms/scroll/ScrollView";
import navigation from "../../../client/state/navigation";
import cons from "../../../client/state/cons";
import './HiddenRooms.scss';
import { joinRuleToIconSrc } from "../../../util/matrixUtil";
import RoomSelector from "../../molecules/room-selector/RoomSelector";
import { roomToUnreadAtom } from "../../state/room/roomToUnread";
import { roomToParentsAtom } from "../../state/room/roomToParents";
function useVisiblityToggle() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleHiddenRoomsOpen = () => {
setIsOpen(true);
};
navigation.on(cons.events.navigation.HIDDEN_ROOMS_OPENED, handleHiddenRoomsOpen);
return () => {
navigation.removeListener(cons.events.navigation.HIDDEN_ROOMS_OPENED, handleHiddenRoomsOpen);
};
}, []);
const requestClose = () => setIsOpen(false);
return [isOpen, requestClose];
}
function mapRoomIds(roomIds, directs, roomIdToParents) {
const mx = initMatrix.matrixClient;
return roomIds.map((roomId) => {
const room = mx.getRoom(roomId);
const parentSet = roomIdToParents.get(roomId);
const parentNames = parentSet ? [] : undefined;
parentSet?.forEach((parentId) => parentNames.push(mx.getRoom(parentId).name));
const parents = parentNames ? parentNames.join(', ') : null;
let type = 'room';
if (room.isSpaceRoom()) type = 'space';
else if (directs.includes(roomId)) type = 'direct';
return {
type,
name: room.name,
parents,
roomId,
room,
};
});
}
function HiddenRooms() {
const [result, setResult] = useState(null);
const [isOpen, requestClose] = useVisiblityToggle();
const mx = initMatrix.matrixClient;
const { navigateRoom, navigateSpace } = useRoomNavigate();
const mDirects = useAtomValue(mDirectAtom);
const rooms = useRooms(mx, allRoomsAtom, mDirects);
const directs = useDirects(mx, allRoomsAtom, mDirects);
const roomToUnread = useAtomValue(roomToUnreadAtom);
const roomToParents = useAtomValue(roomToParentsAtom);
const handleAfterOpen = () => {
setResult(
mapRoomIds(
[...rooms, ...directs].filter((roomId) => isHidden(mx, roomId)),
directs,
roomToParents
)
);
console.log(result);
};
const openItem = (roomId, type) => {
if (type === 'space') navigateSpace(roomId);
else navigateRoom(roomId);
requestClose();
};
const renderRoomSelector = (item) => {
let imageSrc = null;
imageSrc = item.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
return (
<RoomSelector
key={item.roomId}
name={item.name}
parentName={item.parents}
roomId={item.roomId}
imageSrc={imageSrc}
isUnread={roomToUnread.has(item.roomId)}
notificationCount={roomToUnread.get(item.roomId)?.total ?? 0}
isAlert={roomToUnread.get(item.roomId)?.highlight > 0}
onClick={() => openItem(item.roomId, item.type)}
/>
);
};
useBackButton(requestClose);
return (
<RawModal
className="hidden-rooms-dialog__modal dialog-modal"
isOpen={isOpen}
onAfterOpen={handleAfterOpen}
onRequestClose={requestClose}
size="small"
>
<div className="hidden-rooms-dialog">
<div className="hidden-rooms-dialog__content-wrapper">
<ScrollView autoHide>
<div className="hidden-rooms-dialog__content">
{Array.isArray(result) && result.map(renderRoomSelector)}
</div>
</ScrollView>
</div>
</div>
</RawModal>
);
}
export default HiddenRooms;

View file

@ -0,0 +1,80 @@
@use '../../partials/dir';
.hidden-rooms-dialog__modal {
--modal-height: 380px;
height: 100%;
background-color: var(--bg-surface);
}
.hidden-rooms-dialog {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
&__input {
padding: var(--sp-normal);
display: flex;
align-items: center;
position: relative;
& > .ic-raw {
position: absolute;
--away: calc(var(--sp-normal) + var(--sp-tight));
@include dir.prop(left, var(--away), unset);
@include dir.prop(right, unset, var(--away));
}
& > .ic-btn {
border-radius: calc(var(--bo-radius) / 2);
position: absolute;
--away: calc(var(--sp-normal) + var(--sp-extra-tight));
@include dir.prop(right, var(--away), unset);
@include dir.prop(left, unset, var(--away));
}
& .input-container {
min-width: 0;
flex: 1;
}
& input {
padding-left: 40px;
padding-right: 40px;
font-size: var(--fs-s1);
letter-spacing: var(--ls-s1);
line-height: var(--lh-s1);
color: var(--tc-surface-high);
}
}
&__content-wrapper {
min-height: 0;
flex: 1;
position: relative;
&::before,
&::after {
position: absolute;
top: 0;
z-index: 99;
content: "";
display: inline-block;
width: 100%;
height: 8px;
background-image: linear-gradient(to bottom, var(--bg-surface), var(--bg-surface-transparent));
}
&::after {
top: unset;
bottom: 0;
background-image: linear-gradient(to bottom, var(--bg-surface-transparent), var(--bg-surface));
}
}
&__content {
padding: var(--sp-extra-tight);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
}
&__footer {
padding: var(--sp-tight) var(--sp-normal);
text-align: center;
}
}

View file

@ -0,0 +1,47 @@
import { useRef, useState } from 'react';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import './Banner.scss';
import initMatrix from '../../../client/initMatrix';
export default function Banner({ url, noBorder, onUpload, emptyBanner }) {
const mx = useMatrixClient();
const uploadImageRef = useRef(null);
const [uploadPromise, setUploadPromise] = useState(null);
async function uploadImage(e) {
const file = e.target.files.item(0);
if (file === null) return;
try {
const uPromise = initMatrix.matrixClient.uploadContent(file);
setUploadPromise(uPromise);
const res = await uPromise;
if (typeof res?.content_uri === 'string') onUpload(res.content_uri);
setUploadPromise(null);
} catch {
setUploadPromise(null);
}
uploadImageRef.current.value = null;
}
function handleClick() {
if (uploadPromise !== null) return;
uploadImageRef.current?.click();
};
function cancelUpload() {
initMatrix.matrixClient.cancelUpload(uploadPromise);
setUploadPromise(null);
uploadImageRef.current.value = null;
}
return (
<div onClick={handleClick} className={noBorder ? 'banner-container-nb' : 'banner-container'}>
{
!emptyBanner ?
<img src={mx.mxcUrlToHttp(url)} className='profile-banner' /> :
<div style={{ height: '150px', backgroundColor: emptyBanner }} />
}
<input type='file' accept='image/*' onChange={uploadImage} ref={uploadImageRef} style={{ display: 'none' }} />
</div>
);
}

View file

@ -0,0 +1,26 @@
@use '../../partials/flex';
@use '../../partials/dir';
.profile-banner {
max-width: 100%;
// border-radius: 5px;
}
.banner-container {
height: 150px;
overflow-y: hidden;
margin-bottom: 10px;
// border-radius: 5px;
border-width: 2px;
border-style: solid;
border-color: rgb(182, 182, 182);
}
.banner-container-nb {
height: 150px;
overflow-y: hidden;
margin-top: -2px;
margin-left: -2px;
margin-right: -2px;
margin-bottom: 10px;
}

View file

@ -0,0 +1,10 @@
import { useMatrixClient } from '../../hooks/useMatrixClient';
import './Banner.scss';
export default function Banner({ url, noBorder }) {
const mx = useMatrixClient();
return (
<div className={noBorder ? 'banner-container-nb__pv' : 'banner-container__pv'}>
<img src={mx.mxcUrlToHttp(url)} className='profile-banner' />
</div>
);
}

View file

@ -0,0 +1,26 @@
@use '../../partials/flex';
@use '../../partials/dir';
.profile-banner {
max-width: 100%;
// border-radius: 5px;
}
.banner-container__pv {
height: 150px;
overflow-y: hidden;
margin-bottom: 10px;
// border-radius: 5px;
border-width: 2px;
border-style: solid;
border-color: rgb(182, 182, 182);
}
.banner-container-nb__pv {
height: 150px;
overflow-y: hidden;
margin-top: -20px;
margin-left: -20px;
margin-right: -20px;
margin-bottom: 10px;
}

View file

@ -0,0 +1,8 @@
export const removeNotifications = (roomId: string) => {
if (navigator.serviceWorker) {
navigator.serviceWorker.ready.then((registration) => {
if (registration.active)
registration.active.postMessage({ action: 'closeNotification', room_id: roomId });
});
}
};

22
src/fact.tsx Normal file
View file

@ -0,0 +1,22 @@
import { as, Text } from 'folds';
import React from 'react';
import cons from './client/state/cons';
export const RandomFact = as<'span'>(
(
ref
) => {
const facts = [
"First idea was to fork SchildiChat for Android. Not the best idea.",
`${cons.name} is based on Cinny.`,
"What is 'Matrix Specification'? Is it a thing all clients should follow?",
`The only reason of rewriting message composer was mobile compatibility.`
];
const fact = facts[Math.floor(Math.random() * facts.length)];
return (
<Text>
{fact}
</Text>
);
}
);

864
src/lang/en.json Normal file
View file

@ -0,0 +1,864 @@
{
"loginpage.login-title": "Login",
"error.no-login-methods": "This client does not support login on \"{0}\" homeserver. Password and SSO based login method not found.",
"loginpage.register-tip": "Do not have an account?",
"loginpage.register-link": "Register",
"hint.header-text": "Hint",
"hint.username": "Username:",
"hint.mxid": "Matrix ID:",
"hint.email": "Email:",
"form.username": "Username",
"error.login.server_not_allowed": "Login via custom server is not allowed by this client instance.",
"error.login.invalid_server": "Failed to find your Matrix ID server.",
"form.password": "Password",
"error.login.forbidden": "Invalid username or password.",
"error.login.user_deactivated": "This account has been deactivated.",
"error.login.invalid_request": "Failed to login. Your request data is invalid.",
"error.login.rate_limited": "Failed to login. Too many attempts, please try again later.",
"error.login.unknown": "Failed to login.",
"login.forgot_password_link": "Forgot password?",
"login.login_button": "Login",
"error.register.user_taken": "This username is already taken.",
"error.register.user_invalid": "This username contains invalid characters.",
"error.register.user_exclusive": "This username is reserved.",
"error.register.password_weak": "This password is weak.",
"error.register.password_short": "This password is too short",
"form.confirm_password": "Confirm password",
"form.register.token": "Registration token",
"form.register.token_optional": "Registration token (Optional)",
"form.register.email": "Email",
"form.register.email_optional": "Email (Optional)",
"form.register.accept_tos_1": "I accept server ",
"form.register.accept_tos_link": "Terms and Conditions",
"form.register.accept_tos_2": ".",
"error.register.rate_limited": "Failed to register. Too many attempts, please try again later.",
"error.register.forbidden": "Failed to register. This homeserver has disabled registration.",
"error.register.invalid_request": "Failed to register. Your request data is invalid.",
"error.register.unknown_reason": "Failed to register.",
"register.register_button": "Register",
"form.register": "Register",
"error.register.disabled": "Registration has been disabled on this homeserver.",
"error.generic.rate_limited": "Too many attempts, please try again later.",
"error.register.flows_loading_error": "Failed to get any registration options.",
"error.register.unsupported": "Registration on this homeserver is not supported.",
"register.login_tip": "Already have an account?",
"register.login_link": "Login",
"password_reset.success": "Password has been reset.",
"password_reset.login": "Login",
"password_reset.tip": "Homeserver {0} will send you an email to let you reset your password.",
"form.email": "Email",
"password_reset.new": "New password",
"password_reset.confirm": "Confirm password",
"error.password_reset": "Failed to reset password.",
"password_reset.button": "Reset password",
"form.reset_password": "Reset password",
"password_reset.login_tip": "Remember your password?",
"password_reset.login_link": "Login",
"footer.about": "About",
"footer.mx_room": "Matrix Room",
"footer.mx_powered": "Powered by Matrix",
"form.homeserver": "Homeserver",
"form.homeserver_loading": "Looking for homeserver...",
"form.homeserver_error": "Failed to find homeserver.",
"error.hs_failed_to_connect": "Failed to connect. Host {0} is unusable.",
"error.hs_invalid_base": "Failed to connect. Homeserver's base URL is invalid.",
"auth.connecting": "Connecting to {0}",
"error.hs_unavailable": "Failed to connect. Homeserver is unavailable or does not exist.",
"auth.loading_flow": "Loading authentication flow...",
"error.loading_flow": "Failed to get authentication flow information",
"picker.hs_list": "Homeserver list",
"sso.continue_with": "Continue with {0}",
"chats.mark_as_read": "Mark as read",
"direct_menu.title": "Direct messages",
"direct_menu.empty": "No direct messages",
"direct_menu.empty.2": "You do not have any directs messages yet.",
"direct_menu.empty.start_new": "Start a DM",
"direct_menu.new": "New chat",
"direct_menu.chats": "Chats",
"explore.add_server": "Add server",
"explore.add_server.desc": "Add server to explore public communities.",
"input.explore.server_name": "Server name",
"error.explore.load_rooms": "Failed to load public rooms.",
"explore.view": "View",
"explore.add_server_btn": "Add server",
"explore.title": "Explore communities",
"explore.featured": "Featured",
"explore.servers": "Servers",
"explore.featured.title": "Featured",
"explore.featured.subtitle": "Find and explore public rooms and spaces featured by client provider",
"explore.featured_spaces": "Featured spaces",
"explore.featured_rooms": "Featured rooms",
"explore.no_featured": "No rooms or spaces were featured by client provider.",
"explore.server.room_type_filter.all": "All",
"explore.server.room_type_filter.spaces": "Spaces",
"explore.server.room_type_filter.rooms": "Rooms",
"explore.server.search": "Search",
"placeholder.explore.server.search": "Search for keyword",
"btn.clear_search": "Clear",
"btn.submit_search": "Search",
"explore.server.protocols": "Protocols",
"presets": "Presets",
"count.custom_limit": "Custom Limit",
"aria.per_page_item_limit": "Per Page Item Limit",
"count.set_limit": "Apply",
"count.page_limit": "Limit: {0}",
"search.results": "Results for \"{0}\"",
"explore.popular_communities": "Popular communities",
"btn.prev_page": "Previous page",
"btn.next_page": "Next page",
"btn.prev": "Previous",
"btn.next": "Next",
"btn.jump_to_page": "Jump to page",
"explore.server.no_communities": "No communities found!",
"home.title": "Home",
"create.title": " — create {0}",
"create.title.room": "room",
"create.title.space": "space",
"home.empty": "No rooms",
"home.empty.2": "You do not have any rooms yet.",
"home.empty.new": "New room",
"home.empty.explore": "Public rooms",
"home.new_room": "Create room",
"home.join_via_address": "Join via address",
"home.search_messages": "Search messages",
"home.rooms": "Rooms",
"msg_search.title": "Message search",
"inbox.invites": "Invites",
"inbox.invites.title": "Invites",
"inbox.title": "Inbox",
"inbox.notifications": "Notifications",
"inbox.invites.by": "Invited by {0}",
"btn.decline": "Decline",
"btn.accept": "Accept",
"inbox.invites.dm": "Direct messages",
"inbox.invites.space": "Spaces",
"inbox.invites.room": "Rooms",
"inbox.invites.empty": "No invites",
"inbox.invites.empty.2": "You do not have any new pending invitations yet.",
"unknown_event": "{0} event",
"notifications.room_tombstone": "Room Tombstone: {0}",
"notifications.mark_as_read": "Mark as read",
"notifications.open_msg": "Open",
"notifications.title": "Notifications",
"notifications.filter": "Filter",
"notifications.filter.all": "All",
"notifications.filter.highlighted": "Highlighted",
"scroll_to_top": "Scroll to top",
"notifications.empty": "No notifications",
"notifications.empty.2": "You do not have any notifications yet.",
"btn.unpin": "Unpin",
"btn.pin": "Pin",
"space.action.invite": "Invite",
"space.action.copy_link": "Copy link",
"space.action.settings": "Space settings",
"space.action.leave": "Leave space",
"sidebar.tooltip.users": "Users",
"space.lobby": "Lobby",
"rooms": "Rooms",
"nav.create_space": "Create space",
"connecting": "Connecting to server",
"error.hs_connect": "Failed to connect to homeserver. Either homeserver is down or your network connection.",
"btn.retry": "Retry",
"btn.continue": "Continue",
"welcome": "Welcome to <clientName>!",
"welcome.2": "A fork of Cinny with nice improvements.",
"btn.source_code": "Source code",
"btn.sponsor": "Support",
"error.config_load": "Failed to load client configuration.",
"error.unsupported_browser": "Unsupported browser",
"error.indexed_db_unsupported": "No IndexedDB support found. This application requires IndexedDB to store session data locally. Please make sure your browser support IndexedDB and have it enabled.",
"qna.indexeddb": "What is IndexedDB?",
"emojiboard.stickers": "Sticker",
"emojiboard.emojis": "Emoji",
"emojiboard.personal_pack": "Personal pack",
"emojiboard.unknown_pack": "Unknown pack",
"emojigroup.unknown_name": "Unknown",
"emojiboard.no_sticker_packs": "No sticker packs",
"emojiboard.no_sticker_packs.2": "Add stickers via user, room or space settings",
"emojiboard.search_or_text_reaction": "Search emoji or send a text reaction",
"emojiboard.search": "Search",
"emojiboard.react": "Send reaction",
"emojiboard.results": "Search results",
"emojiboard.no_results": "No results found",
"egid_people": "Smileys & People",
"egid_nature": "Animals & Nature",
"egid_food": "Food & Drinks",
"egid_activity": "Activities",
"egid_travel": "Travel & Places",
"egid_object": "Objects",
"egid_symbols": "Symbols",
"egid_flag": "Flags",
"event_readers.seen_by": "Seen by",
"aria.zoom_out": "Zoom out",
"aria.zoom_in": "Zoom in",
"btn.download": "Download",
"leaveroom.title": "Leave room",
"leaveroom.text": "Are you sure you want to leave this room?",
"leaveroom.text.2": "Are you sure you want to leave {0}?",
"error.leaveroom": "Failed to leave room! {0}",
"leaveroom.leaving": "Leaving...",
"btn.leave": "Leave",
"leavespace.title": "Leave space",
"leavespace.text": "Are you sure you want to leave this space?",
"leavespace.text.2": "Are you sure you want to leave \"{0}\" space?",
"error.leavespace": "Failed to leave space! {0}",
"leavespace.leaving": "Leaving...",
"msg.redacted": "This message has been deleted",
"msg.redacted.reason": "This message has been deleted: {0}",
"msg.failed.load": "Failed to load message",
"msg.failed.decrypt": "Failed to decrypt message",
"msg.not_decrypted": "Decryption of this message may take some time",
"msg.unsupported": "Unsupported message",
"msg.broken": "Broken message",
"msg.empty": "Empty message",
"msg.edited": " (edited)",
"msg.file.failed": "Failed to load file",
"btn.open_file": "Open file",
"btn.open_pdf": "Open PDF",
"btn.retry_download": "Retry download ({0})",
"btn.download_size": "Download ({0})",
"btn.view": "View",
"msg.image.failed": "Failed to load image",
"msg.thumbnail.failed": "Failed to load thumbnail",
"btn.watch": "Watch",
"msg.video.failed": "Failed to load video",
"generic.delimiter": ", ",
"generic.and": " and ",
"generic.unknown": "[Unknown]",
"generic.others": "{0} others",
"reaction.reacted_with": " reacted with ",
"time.yesterday": "Yesterday, {0}",
"error.pdfviewer": "Failed to load PDF",
"aria.page_number": "Page number",
"btn.cancel": "Cancel",
"generic.space": "Space",
"generic.room": "Room",
"generic.member_count": "{0} members",
"room_card.joining": "Joining...",
"btn.join": "Join",
"btn.error_details": "Error details",
"error.join.unknown": "Failed to join.",
"room_intro.1": "This is beginning of conversation.",
"room_intro.2": "Created by {0} on {1}",
"btn.invite": "Invite",
"btn.old_room.open": "Open old room",
"btn.old_room.join": "Join old room",
"btn.copy_all": "Copy all",
"error.register.unknown": "Failed to register.",
"btn.verify_email": "Verify email",
"email_stage.verifying": "Verifying email...",
"error.verify_email.unknown": "Failed to verify email.",
"error.email_stage": "Verification error",
"email_stage.sent": "Verification link sent",
"email_stage.sent.2": "Check your email ({0}) and follow verification instructions. (Do not forget to check Spam folder)",
"recaptcha_stage.error": "Invalid captcha",
"recaptcha_stage.error.2": "Invalid captcha was received.",
"recaptcha_stage.tip": "Please check the box below to proceed.",
"form.reg_token": "Registration token",
"error.reg_token.invalid": "Invalid registration token provided.",
"error.reg_token.1.title": "Token required",
"error.reg_token.1.msg": "Please enter registration token provided by homeserver admin.",
"error.tos": "Failed to accept Terms of Service.",
"upload_board.title": "Files",
"btn.ub_send": "Send",
"error.upload": "Failed to upload",
"btn.ub_remove": "Remove",
"btn.ub_remove_all": "Remove all",
"aria.retry_upload": "Retry uploading",
"aria.cancel_upload": "Cancel uploading",
"error.load_auth_flow": "Failed to load auth flow!",
"error.missing_auth_flow": "Missing auth flow!",
"generic.step": "Step {0}",
"generic.step.2": "Step {0} of {1}",
"btn.exit": "Exit",
"btn.space_lobby.suggest": "Mark as suggested",
"btn.space_lobby.unsuggest": "Unmark as suggested",
"btn.space_lobby.remove_room": "Remove",
"btn.space_lobby.invite": "Invite",
"btn.space_lobby.settings": "Settings",
"btn.space.pin": "Pin to sidebar",
"btn.space.unpin": "Unpin from sidebar",
"aria.scroll_to_top": "Scroll to top",
"generic.members": "Members",
"generic.more_options": "More options",
"space_room.suggested": "Suggested",
"space_room.private": "Private",
"space_item.suggested": "Suggested",
"space_item.private": "Private",
"space_item.rooms": "Rooms",
"btn.space.new_room": "New room",
"btn.space.existing_room": "Existing room",
"btn.space.new_space": "New space",
"btn.space.existing_space": "Existing space",
"btn.space.add_room": "Add room",
"btn.space.add_space": "Add space",
"msg_search.title.2": "Search messages",
"msg_search.subtitle.2": "Find helpful messages by searching related keywords.",
"generic.no_results": "No results found for {0}",
"generic.no_results.2": "No results found",
"generic.results": "Results for \"{0}\"",
"generic.sort_by": "Sort by",
"sort.recent": "Recent",
"sort.relevance": "Relevance",
"form.search.query": "Search",
"generic.results.room": "Rooms for \"{0}\"",
"generic.no_matches": "No matches found",
"generic.no_matches.2": "No matches found for \"{0}\"",
"btn.save": "Save",
"btn.save.2": "Save ({0})",
"btn.deselect_all": "Deselect all",
"search_filters.select_rooms": "Select rooms",
"search_filters.global": "Global",
"search_input.header": "Search",
"placeholder.search_input": "Search for keyword",
"btn.search.clear": "Clear",
"search_input.enter": "Enter",
"btn.search_result.open": "Open",
"msg_menu.view_reactions": "View reactions",
"msg_menu.read_receipts": "Read receipts",
"msg_menu.view_source": "View source",
"msg_menu.recover": "Recover",
"error.recover": "Your homeserver does not implement Synapse Admin API or you do not have admin rights",
"msg_menu.copy_link": "Copy link",
"msg_redact.title": "Delete message",
"msg_redact.subtitle": "This action is irreversible! Are you sure you want to delete this message?",
"msg_redact.reason.1": "Reason ",
"msg_redact.reason.2": "(optional)",
"error.redact_msg": "Failed to delete message! Please try again.",
"msg_redact.processing": "Deleting...",
"btn.msg_redact": "Delete",
"msg_menu.redact": "Delete",
"msg_report.title": "Report message",
"msg_report.subtitle": "Report this message to homeserver admins, which may then notify the appropriate people to take an action.",
"msg_report.reason": "Reason",
"error.msg_report": "Failed to report message! Please try again.",
"success.msg_report": "Message has been reported.",
"msg_report.processing": "Reporting...",
"btn.msg_report": "Report",
"msg.too_many_buttons": "Too many buttons",
"msg_menu.end_poll": "End poll",
"msg_menu.add_reaction": "Add reaction",
"msg_menu.reply": "Reply",
"msg_menu.edit": "Edit",
"placeholder.msg_edit": "Edit message...",
"btn.msg_edit.save": "Edit",
"reaction_viewer.reacted_with": "Reacted with :{0}:",
"autocomplete.commands.title": "Commands",
"autocomplete.commands.tip": "Begin your message with a command",
"members_drawer.joined": "Joined",
"members_drawer.invited": "Invited",
"members_drawer.left": "Left",
"members_drawer.kicked": "Kicked",
"members_drawer.banned": "Banned",
"sort.a_to_z": "A to Z",
"sort.z_to_a": "Z to A",
"sort.newest": "Newest",
"sort.oldest": "Oldest",
"tooltip.close": "Close",
"btn.close": "Close",
"placeholder.search_name": "Search by name...",
"generic.result_count": "{0} result(s)",
"members_drawer.no_members.joined": "No joined members",
"members_drawer.no_members.invited": "No invited members",
"members_drawer.no_members.left": "No left members",
"members_drawer.no_members.kicked": "No kicked members",
"members_drawer.no_members.banned": "No banned members",
"room_input.drop_files": "Send files into {0}",
"room_input.drop_files.this_room": "this room",
"room_input.drop_files.2": "Drag and drop files here or click for selection dialog",
"placeholder.room_input": "Send a message...",
"event.room_name": "{0} changed room name",
"event.room_topic": "{0} changed room topic",
"event.room_avatar": "{0} changed room avatar",
"timeline.unknown_state_event": "{0} sent {1} state event",
"timeline.unknown_event": "{0} sent {1} event",
"timeline.new_messages_divider": "New messages",
"timeline.today_divider": "Today",
"timeline.yesterday_divider": "Yesterday",
"btn.timeline.jump_to_unread": "Jump to unread",
"btn.timeline.mark_as_read": "Mark as read",
"btn.timeline.jump_to_latest": "Jump to latest",
"room_tombstone.default_reason": "This room has been replaced and is no longer active.",
"error.tombstone.join_failed": "Failed to join replacement room",
"btn.room_tombstone.new_room": "Open new room",
"btn.room_tombstone.join_room": "Join new room",
"following.one": "{0} is following the conversation.",
"following.two": "{0} and {1} are following the conversation.",
"following.three": "{0}, {1} and {2} are following the conversation.",
"following.more": "{0}, {1}, {2} and {3} are following the conversation.",
"typing.one": "{0} is typing...",
"typing.two": "{0} and {1} are typing...",
"typing.three": "{0}, {1} and {2} are typing...",
"typing.more": "{0}, {1}, {2} and {3} are typing...",
"room_header.mark_as_read": "Mark as read",
"room_header.invite": "Invite",
"room_header.copy_link": "Copy link",
"room_header.settings": "Room settings",
"room_settings.header": "{0}{1}",
"room_settings.header.1": " — room settings",
"room_header.leave": "Leave",
"tooltip.search": "Search",
"tooltip.members": "Members",
"tooltip.more_options": "More options",
"msg_preview.redacted": "[Message deleted]",
"msg_preview.failed": "[Unable to preview]",
"rename_pack.title": "Rename pack",
"btn.rename_pack": "Rename",
"delete_pack.title": "Delete pack",
"delete_pack.desc": "Are you sure that you want to delete \"{0}\"?",
"btn.delete_pack": "Delete",
"image_pack.image": "Image",
"image_pack.shortcode": "Shortcode",
"image_pack.usage": "Usage",
"generic.view_less": "View less",
"generic.view_more": "View more",
"generic.view_more.count": "View {0} more",
"image_pack.global_use.1": "Use globally",
"image_pack.global_use.2": "Be able to use this pack in all rooms.",
"image_pack.global": "Global packs",
"image_pack.no_global": "No global packs",
"image.usage.emoji": "Emoji",
"image.usage.sticker": "Sticker",
"image.usage.both": "Both",
"label.name": "Name",
"label.attribution": "Attribution",
"tooltip.edit": "Edit",
"image_pack.pack_usage": "Pack usage",
"tooltip.remove_file": "Remove file",
"btn.import_image": "Import image",
"generic.uploading": "Uploading...",
"btn.upload": "Upload",
"img_upload.upload": "Upload",
"img_upload.cancel": "Cancel",
"img_upload.remove": "Remove",
"placeholder.confirm_password": "Confirm password",
"placeholder.password": "Password",
"btn.export_keys": "Export",
"error.export_keys": "Failed to export keys. Please try again.",
"success.export_keys": "Successfully exported all keys.",
"error.passwords_didnt_match": "Passwords did not match.",
"export_keys.getting": "Getting keys...",
"export_keys.encrypting": "Encrypting keys...",
"import_keys.decrypting": "Decrypting file...",
"import_keys.msg_decrypting": "Decrypting messages...",
"success.import_keys": "Successfully imported all keys.",
"error.import_keys": "Failed to decrypt keys. Please try again.",
"btn.import_keys": "Import keys",
"btn.import_keys.decrypt": "Decrypt",
"pl_selector.presets": "Presets",
"power_level.admin": "Admin - 100",
"power_level.mod": "Mod - 50",
"power_level.member": "Member - 0",
"error.invalid_characters.a-zA-Z0-9_-": "Invalid characters. Use only letters, numbers, underscores and dashes.",
"room_aliases.validating": "Validating {0}...",
"room_aliases.available": "{0} is available.",
"room_aliases.used": "{0} is already in use.",
"room_aliases.deleting": "Deleting...",
"btn.room_aliases.delete": "Delete",
"btn.room_aliases.main": "Set as main",
"btn.room_aliases.publish": "Publish",
"btn.room_aliases.unpublish": "Un-publish",
"room_aliases.main": "Main",
"room_aliases.publish": "Publish {0} to the {1}'s public room directory?",
"room_aliases.publish.room": "this room",
"room_aliases.publish.space": "this space",
"room_aliases.published": "Published addresses",
"room_aliases.no_published": "No published addresses",
"room_aliases.no_main_address": "No main address. Select one from below.",
"room_aliases.message": "Published addresses can be used by anyone on any server to join {0}. To publish an address, it needs to be set as a local address first.",
"room_aliases.message.room": "this room",
"room_aliases.message.space": "this space",
"room_aliases.local": "Local addresses",
"room_aliases.no_local": "No local addresses",
"room_aliases.message.2": "Set local addresses for {0} to allow users from your homeserver finding it.",
"room_aliases.message.room.2": "this room",
"room_aliases.message.space.2": "this space",
"room_aliases.add_local": "Add local address",
"btn.add": "Add",
"btn.show_local_address": "Add / view local addresses",
"btn.hide_local_address": "Hide local addresses",
"room_emojis.create": "Create pack",
"placeholder.create_pack_name": "Pack name",
"btn.room_emojis.create_pack": "Create pack",
"room_emojis.empty": "No emoji or stickers pack yet.",
"room_encryption.confirm.1": "It is not recommended to enable encryption in public rooms. Anyone can find and join public rooms and read messages in them.",
"room_encryption.confirm.2": "Once enabled, encryption for a room cannot be disabled. Encrypted messages cannot be seen by the server, only by members of this room. This will break most bridges and bots.",
"dialog.room_encryption.title": "Enable encryption",
"btn.dialog.room_encryption.1": "Continue",
"btn.dialog.room_encryption.2": "Enable",
"room_encryption.setting.title": "Enable room encryption",
"room_encryption.setting.tip": "Once enabled, encryption cannot be disabled.",
"history_visibility.anyone": "Anyone (including guests)",
"history_visibility.members": "Members (all messages)",
"history_visibility.invited": "Members (messages after invite)",
"history_visibility.joined": "Members (messages after join)",
"history_visibility.tip": "Change to history visibility will only apply to future messages. The visibility of existing history will have no effect.",
"room_members.search_title": "Search members",
"placeholder.search_room_members": "Search by name",
"room_members.1": "{0} members",
"room_members.found": "Found {0} members",
"room_members.joined": "Joined",
"room_members.invited": "Invited",
"room_members.banned": "Banned",
"room_members.empty": "No members to display",
"room_notifications.global": "Default",
"room_notifications.all": "All messages",
"room_notifications.mentions": "Mentions & keywords",
"room_notifications.mute": "None",
"perms.default.title": "Default power level",
"perms.default.desc": "Set default power level for new members.",
"perms.send.title": "Send messages",
"perms.send.desc": "Set minumum power level to send messages.",
"perms.react.title": "Send reactions",
"perms.react.desc": "Set minimum power level to send reactions.",
"perms.redact.title": "Delete messages",
"perms.redact.desc": "Set minimum power level to delete messages.",
"perms.ping_room.title": "Ping everyone (@room)",
"perms.ping_room.desc": "Set minimum power level to ping @room.",
"perms.manage_rooms.title": "Manage rooms",
"perms.manage_rooms.desc": "Set minimum power level to manage rooms in space.",
"perms.invite.title": "Invite",
"perms.invite.desc": "Set minimum power level to invite members.",
"perms.kick.title": "Kick",
"perms.kick.desc": "Set minimum power levels to kick members.",
"perms.ban.title": "Ban",
"perms.ban.desc": "Set minimum power level to ban members.",
"perms.avatar.title": "Change room avatar",
"perms.avatar.desc": "Set minimum power level to change room/space avatar.",
"perms.name.title": "Change room name",
"perms.name.desc": "Set minimum power level to change room/space name.",
"perms.topic.title": "Change room topic",
"perms.topic.desc": "Set minimum power level to change room/space topic.",
"perms.state.title": "Change settings",
"perms.state.desc": "Set minimum power level to change room settings.",
"perms.canonical.title": "Change published addresses",
"perms.canonical.desc": "Set minimum power level to publish and set main address.",
"perms.power_levels.title": "Change permissions",
"perms.power_levels.desc": "Set minimum power level to change permissions.",
"perms.encryption.title": "Enable encryption",
"perms.encryption.desc": "Set minimum power level to enable room encryption.",
"perms.history.title": "Change history visibility",
"perms.history.desc": "Set minimum power level to change room history visibility.",
"perms.tombstone.title": "Upgrade room",
"perms.tombstone.desc": "Set minimum power level to upgrade room.",
"perms.pinned.title": "Pin messages",
"perms.pinned.desc": "Set minimum power level to pin messages in room.",
"perms.server_acl.title": "Change banned servers",
"perms.server_acl.desc": "Set minimum power level to change banned servers.",
"perms.widgets.title": "Modify widgets",
"perms.widgets.desc": "Set minimum power level to modify room widgets.",
"room_perms.general": "General permissions",
"room_perms.members": "Manage members",
"room_perms.room_profile": "Room profile permissions",
"room_perms.settings": "Settings permissions",
"room_perms.other": "Other permissions",
"space_perms.space_profile": "Space profile permissions",
"generic.pl_member": "Member",
"room_profile.saving_name": "Saving room name...",
"room_profile.saving_topic": "Saving room topic...",
"room_profile.saved": "Saved successfully",
"error.room_profile": "Failed to save.",
"confirm.remove_room_avatar.title": "Remove room avatar",
"confirm.remove_room_avatar.desc": "Are you sure that you want to remove room avatar?",
"btn.remove_room_avatar": "Remove",
"room_profile.only": "You have permission only to change {0} {1}.",
"room_profile.only.room": "this room",
"room_profile.only.space": "this space",
"room_profile.only.name": "name",
"room_profile.only.topic": "topic",
"btn.room_profile.save": "Save",
"room_tile.invited": "Invited by {0} to {1}{2}",
"room_tile.members": " • {0} members",
"room_tile.info": "{0}{1}",
"room_visibility.invite": "Invite only",
"room_visibility.restricted": "Space members only",
"room_visibility.restricted.unsupported": "Space members only (Room upgrade required)",
"room_visibility.public": "Public",
"space_add_existing.adding": "Adding {0} items...",
"placeholder.search_room": "Search room",
"btn.space_add_existing.add": "Add",
"space_add_existing.item_selected": "{0} item(s) selected",
"space_add_existing.tip": " — add existing {0}",
"space_add_existing.tip.rooms": "rooms",
"space_add_existing.tip.spaces": "spaces",
"error.create_room.invalid_characters": "Error: Invalid characters in address",
"error.create_room.address_already_used": "Error: This address is already used",
"create_room.join_rule.short.private": "Private",
"create_room.join_rule.short.restricted": "Restricted",
"create_room.join_rule.short.public": "Public",
"create_room.join_rule.private": "Invite only",
"create_room.join_rule.restricted": "Space member can join",
"create_room.join_rule.public": "Anyone can join",
"create_room.join_rule": "Visibility (who can join)",
"create_room.join_rule.title": "Visibility",
"create_room.join_rule.tip": "Select who can join {0}",
"create_room.join_rule.tip.room": "this room",
"create_room.join_rule.tip.space": "this space",
"create_room.address.room": "Room address",
"create_room.address.space": "Space address",
"error.create_room.address_in_use": "{0} is already in use",
"create_room.encrypt.title": "Enable end-to-end encryption",
"create_room.encrypt.desc": "You can't disable this later. Bridges and most bots won't work yet.",
"create_room.your_pl.title": "Select your power level",
"create_room.your_pl.desc": "Admin - 100. Founder - 101. Goku - 9001.",
"create_room.your_pl.admin": "Admin",
"create_room.your_pl.founder": "Founder",
"create_room.your_pl.goku": "Goku",
"create_room.topic": "Topic (optional)",
"create_room.name": "{0} name",
"create_room.name.room": "Room",
"create_room.name.space": "Space",
"btn.create_room": "Create",
"create_room.creating": "Creating...",
"emoji_verification.waiting": "Waiting for response from other device...",
"emoji_verification.tip": "Confirm the emoji below are displayed on both devices, in the same order:",
"btn.emoji_verification.they_match": "They match",
"btn.emoji_verification.no_match": "They don't match",
"emoji_verification.outgoing": "Please accept the request on other device.",
"emoji_verification.ingoing": "Accept to begin verification process.",
"btn.emoji_verification.accept": "Accept",
"emoji_verification.title": "Emoji verification",
"error.invite.not_found": "{0} not found!",
"btn.open": "Open",
"invite.joined": "Already joined",
"invite.invited": "Already invited",
"invite.banned": "Banned",
"btn.dm": "Message",
"invite.title": "Invite to {0}",
"invite.dm": "Direct message",
"btn.invite.search": "Search",
"label.name_or_id": "Name or Matrix ID",
"invite.searching": "Searching for user \"{0}\"...",
"invite.result": "Search result for user \"{0}\"",
"error.join_alias.invalid": "Invalid address.",
"join_alias.looking": "Looking for address...",
"join_alias.joining": "Joining {0}...",
"error.join_alias.not_found": "Unable to find room or space with address {0}. Either room/space is private or does not exist.",
"error.join_alias.unable_to_join": "Unable to join {0}. Either room/space is private or does not exist.",
"btn.join_alias.join": "Join",
"join_alias.title": "Join via address",
"profile_editor.remove_avatar.title": "Remove avatar",
"profile_editor.remove_avatar.desc": "Are you sure you want to remove avatar?",
"btn.profile_editor.remove_avatar": "Remove",
"profile_editor.displayname": "Your display name",
"btn.profile_editor.save_name": "Save",
"label.profile_viewer.kick_reason": "Kick reason",
"label.profile_viewer.ban_reason": "Ban reason",
"btn.profile_viewer.kick": "Kick",
"btn.profile_viewer.ban": "Ban",
"session_info.loading": "Loading sessions...",
"session_info.none": "No sessions found.",
"session_info.item": "View {0} session(s)",
"btn.profile_footer.dm": "Message",
"profile_footer.dm.creating": "Creating DM...",
"btn.profile_footer.unban": "Unban",
"btn.profile_footer.disinviting": "Disinviting...",
"btn.profile_footer.disinvite": "Disinvite",
"btn.profile_footer.unignoring": "Unignoring...",
"btn.profile_footer.unignore": "Unignore",
"btn.profile_footer.ignoring": "Ignoring...",
"btn.profile_footer.ignore": "Ignore",
"btn.profile_footer.inviting": "Inviting...",
"btn.profile_footer.invite": "Invite",
"shared_pl_warning": "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?",
"self_demote_warning": "You will not be able to undo this change as you are demoting yourself. Are you sure?",
"profile_viewer.change_power_level.title": "Change power level",
"btn.profile_viewer.change_pl": "Change",
"profile_viewer.power_level": "Power level",
"room_settings.general": "General",
"room_settings.members": "Members",
"room_settings.emojis": "Emojis",
"room_settings.permissions": "Permissions",
"room_settings.security": "Security",
"room_settings.menuheader.notification": "Notification (only for you)",
"room_settings.menuheader.visibility": "Room visibility (who can join)",
"space_settings.visibility": "Room visibility (who can join)",
"space_settings.addresses": "Space addresses",
"room_settings.menuheader.addresses": "Room addresses",
"room_settings.menuheader.encryption": "Encryption",
"room_settings.menuheader.history": "Message history visibility",
"placeholder.search_chats": "Search",
"search.tip": "Type # for rooms, @ for DMs and * for spaces. Hotkey: Ctrl + K",
"error.auth_request": "Wrong password.",
"error.auth_request.failed": "Request failed!",
"label.auth_request.password": "Account password",
"btn.auth_request.continue": "Continue",
"error.crosssigning": "Faield to setup cross-signing. Please try again.",
"error.crosssigning.title": "Setup cross-signing",
"btn.copy": "Copy",
"crosssigning.tip": "Please save this security key somewhere safe. Treat it like password.",
"crosssigning.key.title": "Security key",
"crosssigning.title": "Setup cross-signing",
"error.password.12345678": "How about 87654321?",
"error.password.87654321": "You are playing with fire",
"error.password.8127.ns": "Password must contain 8-127 characters without spaces.",
"error.password.dontmatch": "Passwords did not match.",
"crosssigning.intro": "We will generate a {0}, which you can use to manage messages backup and session verification.",
"crosssigning.intro.security_key": "Security Key",
"btn.crosssigning.generate": "Generate key",
"generic.OR": "OR",
"crosssigning.intro.2": "Alternatively you can also set a Security Phrase so you don't have to remember long Security Key, and optionally save the Key as backup.",
"label.security_phrase": "Security password",
"label.security_phrase.confirm": "Confirm password",
"btn.set_phrase_and_generate_key": "Set Phrase & Generate Key",
"reset_cross_signing.title": "Reset cross-signing",
"reset_cross_signing.warning": "Resetting cross-signing keys is permanent.",
"reset_cross_signing.warning.2": "Anyone you have verified with will see security alerts and your message backups will be lost. You almost certainly do not want to do this, unless you have lost Security Key or Phrase and every session you can cross-sign from.",
"btn.reset_cross_signing": "Reset",
"btn.setup_cross_signing": "Setup",
"cross_signing_tile.title": "Cross-signing",
"cross_signing_tile.desc": "Setup to verify and keep track of all your sessions. Also required to backup encrypted messages.",
"btn.save_device_name": "Save",
"label.session_name": "Session name",
"text.edit_session_name": "Edit session name",
"loading_sessions": "Loading sessions...",
"device_manage.logout.title": "Logout {0}",
"device_manage.logout.warning": "You are about to logout \"{0}\" session.",
"btn.logout": "Logout",
"text.session.current": "Current",
"tooltip.rename_session": "Rename",
"tooltip.remove_session": "Remove session",
"device_manage.last_activity": "Last activity {0}{1}",
"device_manage.last_activity.ip": " at {0}",
"text.session.unverified": "Unverified sessions",
"device_manage.verify.title": "Verify this session either with your Security Key/Phrase here or by initiating emoji verification from a verified session.",
"device_manage.tip": "Verify other sessions by emoji verification or remove unfamiliar ones.",
"device_manage.crosssigning_tip": "Setup cross-signing in case you lose all your sessions.",
"text.session.no_unverified": "No unverified sessions",
"text.session.no_encryption": "Sessions without encryption support",
"text.session.no_verified": "No verified sessions",
"device_manage.name_warning": "Session names are visible to everyone, so do not put any private info here.",
"key_backup.creating": "Creating backup...",
"key_backup.created": "Successfully created backup",
"error.key_backup": "Failed to create backup",
"key_backup.restoring": "Restoring backup keys... ({0}/{1})",
"key_backup.success": "Successfully restored backup keys ({0}/{1}).",
"error.key_backup.invalid_key": "Failed to restore backup. Key is invalid!",
"error.key_backup.unknown": "Failed to restore backup.",
"key_backup.restoring.2": "Restoring backup keys...",
"key_backup.delete.warning": "Deleting key backup is permanent.",
"key_backup.delete.warning.2": "All encrypted messages keys stored on server will be deleted.",
"btn.key_backup.delete": "Delete",
"key_backup.create.title": "Create key backup",
"key_backup.restore.title": "Restore key backup",
"key_backup.delete.title": "Delete key backup",
"tooltip.restore_backup": "Restore backup",
"btn.key_backup.create": "Create backup",
"crosssigning.tip.2": "Setup cross-signing to backup your encrypted messages.",
"key_backup.tip": "Create online backup of your encrypted messages with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.",
"key_backup.title": "Backup encrypted messages",
"settings.remove_wallpaper.title": "Remove wallpaper",
"settings.remove_wallpaper.desc": "Are you sure you want to remove current wallpaper?",
"btn.remove_wallpaper": "Remove",
"settings.theme.header": "Theme",
"settings.system_theme.title": "Follow system theme",
"settings.system_theme.desc": "Use light or dark mode based on the system settings.",
"settings.twemoji.title": "Use Twitter emoji",
"settings.twemoji.desc": "Use Twitter emoji instead of system emoji",
"settings.wallpaper.title": "Chat background",
"settings.wallpaper.desc": "Set background for all chats.",
"btn.settings.wallpaper.change": "Change",
"btn.settings.wallpaper.add": "Add",
"btn.settings.wallpaper.delete": "Delete",
"settings.messages.header": "Messages",
"settings.msg_layout.title": "Message layout",
"settings.msg_layout.modern": "Modern",
"settings.msg_layout.compact": "Compact",
"settings.msg_layout.bubble": "Bubble",
"settings.msg_spacing.title": "Message spacing",
"settings.enter_newline.title": "Use ENTER for line break",
"settings.enter_newline.desc": "Use {0} + ENTER to send message and ENTER for line break.",
"settings.md_formatting.title": "Markdown formatting",
"settings.md_formatting.desc": "Format messages with markdown syntax before sending.",
"settings.hide_membership.title": "Hide membership events",
"settings.hide_membership.desc": "Hide membership change messages from room timeline. (Join, Invite, Leave, Kick and Ban)",
"settings.hide_profile.title": "Hide profile change events",
"settings.hide_profile.desc": "Hide nick and avatar change messages from room timeline.",
"settings.no_media_autoload.title": "Disable media auto load",
"settings.no_media_autoload.desc": "Prevent images and videos from automatically loading to save bandwidth.",
"settings.url_preview.title": "Preview URLs",
"settings.url_preview.desc": "Show preview for links in messages.",
"settings.url_preview_enc.title": "Preview URLs in encrypted rooms",
"settings.url_preview_enc.desc": "Show preview for links in encrypted messages.",
"settings.hidden_events.title": "Show hidden events",
"settings.hidden_events.desc": "Show hidden state and message events.",
"settings.presence.header": "Presence",
"settings.status.title": "Presence status",
"settings.status.online": "Online",
"settings.status.offline": "Offline",
"settings.status.unavailable": "AFK",
"settings.ghost.title": "Ghost mode",
"settings.ghost.desc": "Stop sending read receipts and typing status.",
"settings.status_message.title": "Status message",
"settings.status_message.text": "Enter your status message.",
"btn.status_message.set": "Set",
"settings.extera.header": "Extra settings",
"settings.hide_ads.title": "Hide advertisements",
"settings.hide_ads.desc": "Hide advertisements in channels bridged from Telegram.",
"settings.captions.title": "Captions (WIP)",
"settings.captions.desc": "Send captions and files in one message.",
"settings.smooth_scroll.title": "Smooth scroll",
"settings.smooth_scroll.desc": "Scroll smoothly when receiving a new message.",
"settings.rename_tg_bot.title": "Rename Telegram bridge bot in channels",
"settings.rename_tg_bot.desc": "Make Telegram bridge bot use room name and avatar in channels.",
"settings.notifications.unsupported": "Notifications are not supported in this browser.",
"btn.notifications.request_permission": "Request permission",
"settings.notifications.header": "Notifications & sounds",
"settings.desktop_notifications.title": "Desktop notifications",
"settings.desktop_notifications.desc": "Show desktop notification when new messages arrive.",
"settings.notification_sound.title": "Notification sound",
"settings.notification_sound.desc": "Play sound when new messages arrive.",
"settings.cross_signing.header": "Cross-signing and backup",
"settings.encryption_keys.header": "Export/Import encryption keys",
"settings.export.title": "Export E2E room keys",
"settings.import.title": "Import E2E room keys",
"settings.export.desc": "Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.",
"settings.import.desc": "To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you'll have to enter the password you set in order to decrypt it.",
"tab.appearance": "Appearance",
"tab.presence": "Presence",
"tab.notifications": "Notifications",
"tab.emoji": "Emoji & stickers",
"tab.security": "Security",
"tab.extera": "Extra",
"tab.about": "About",
"logout.title": "Logout",
"logout.confirm": "Are you sure that you want to logout your session?",
"btn.logout.confirm": "Logout",
"remove_banner.title": "Remove banner",
"remove_banner.desc": "Are you sure that you want to remove your banner?",
"btn.remove_banner.confirm": "Remove",
"settings.title": "Settings",
"btn.remove_banner": "Remove banner",
"btn.logout_session": "Logout",
"space_settings.title": "{0}{1}",
"space_settings.title.1": " — space settings",
"room_aliases.publish.title": "Publish to room directory",
"command.me.desc": "Send action message",
"command.notice.desc": "Send notice message",
"command.emote.desc": "Add {0} to your message",
"command.startdm.desc": "Start DM with user. Example: /startdm @amia:example.org",
"command.join.desc": "Join a room via address. Example: /join #extera:officialdakari.ru",
"command.invite.desc": "Invite a user to this room. Example: /invite @amia:example.org",
"command.disinvite.desc": "Revoke invite to room. Example: /disinvite @amia:example.org",
"command.kick.desc": "Kick a user from this room. Example: /kick @amia:example.org",
"command.ban.desc": "Ban a user from this room. Example: /ban @amia:example.org",
"command.unban.desc": "Unban a user from this room. Example: /unban @amia:example.org",
"command.ignore.desc": "Ignore a user. Example: /ignore @amia:example.org",
"command.unignore.desc": "Stop ignoring a user. Example: /unignore @amia:example.org",
"command.localnick.desc": "Change your display name in this room.",
"command.localavatar.desc": "Change your profile picture in this room. Example: /localavatar mxc://officialdakari.ru/slemrxtUERwSCLINehUdKiZk",
"command.converttodm.desc": "Convert this room to DM",
"command.converttoroom.desc": "Convert DM to room",
"command.premium.desc": "Guide on how to get Premium",
"command.hide.desc": "Stop showing this chat in list",
"command.unhide.desc": "Show this chat in list again",
"command.hiddenlist.desc": "View hidden chats",
"tooltip.pinned": "Pinned messages",
"pinned.none": "No pinned messages",
"pinned.title": "Pinned messages",
"msg_menu.pin": "Pin",
"msg_menu.unpin": "Unpin",
"recovered.title": "Recovered message",
"search_input.clear": "Clear",
"settings.presence.title": "Presence status"
}

54
src/lang/index.jsx Normal file
View file

@ -0,0 +1,54 @@
import React from 'react';
const variables = {
clientName: cons.name
};
const supported = [
'en',
'ru'
];
const defaultLanguage = 'en';
import ru from './ru.json';
import en from './en.json';
import cons from '../client/state/cons';
const langs = {
ru, en
};
var lang = {};
const language = navigator.languages.find(x => supported.includes(x)) ?? defaultLanguage;
lang = langs[language];
export const translate = (key, ...elements) => {
var text = (lang[key] ?? key);
for (const key in variables) {
text = text.replaceAll(`<${key}>`, variables[key]);
}
const parts = text.split(/{\d+}/g); // Разделяем строку по шаблону {0}, {1}, и т.д.
const els = [];
// Добавляем текстовые части и элементы поочередно
parts.forEach((part, index) => {
els.push(part);
if (index < elements.length) {
els.push(elements[index]);
}
});
return <>{els}</>;
};
export const getText = (key, ...args) => {
var text = lang[key] ?? key;
for (const key in variables) {
text = text.replaceAll(`<${key}>`, variables[key]);
}
for (let i = 0; i < args.length; i++) {
text = text.replaceAll(`{${i}}`, args[i]);
}
return text;
};

864
src/lang/ru.json Normal file
View file

@ -0,0 +1,864 @@
{
"loginpage.login-title": "Вход",
"error.no-login-methods": "Вход на сервере \"{0}\" не поддерживается. Вход по паролю или через другие сервисы недоступны.",
"loginpage.register-tip": "Нет аккаунта?",
"loginpage.register-link": "Регистрация",
"hint.header-text": "Подсказка",
"hint.username": "Имя пользователя:",
"hint.mxid": "Matrix ID:",
"hint.email": "Электронная почта:",
"form.username": "Имя пользователя",
"error.login.server_not_allowed": "Вход через сторонний сервер недоступен на этом экземпляре клиента.",
"error.login.invalid_server": "Не удалось найти Ваш сервер по Matrix ID.",
"form.password": "Пароль",
"error.login.forbidden": "Неверное имя пользователя или пароль.",
"error.login.user_deactivated": "Этот аккаунт деактивирован.",
"error.login.invalid_request": "Не удалось войти. Данные запроса были повреждены.",
"error.login.rate_limited": "Не удалось войти. Слишком много попыток, попробуйте ещё раз позже.",
"error.login.unknown": "Не удалось войти.",
"login.forgot_password_link": "Забыли пароль?",
"login.login_button": "Войти",
"error.register.user_taken": "Это имя пользователя уже занято.",
"error.register.user_invalid": "Это имя пользователя содержит запрещённые символы.",
"error.register.user_exclusive": "Это имя пользователя зарезервировано.",
"error.register.password_weak": "Этот пароль слабый.",
"error.register.password_short": "Этот пароль слишком короткий",
"form.confirm_password": "Подтвердить пароль",
"form.register.token": "Токен регистрации",
"form.register.token_optional": "Токен регистрации (необязательно)",
"form.register.email": "Email",
"form.register.email_optional": "Email (необязательно)",
"form.register.accept_tos_1": "Я принимаю ",
"form.register.accept_tos_link": "Условия использования",
"form.register.accept_tos_2": " сервера.",
"error.register.rate_limited": "Не удалось зарегистрироваться. Слишком много попыток, попробуйте ещё раз позже.",
"error.register.forbidden": "Не удалось зарегистрироваться. Регистрация отключена на этом домашнем сервере.",
"error.register.invalid_request": "Не удалось зарегистрироваться. Данные запроса были повреждены.",
"error.register.unknown_reason": "Не удалось зарегистрироваться.",
"register.register_button": "Зарегистрироваться",
"form.register": "Регистрация",
"error.register.disabled": "Регистрация отключена на этом домашнем сервере.",
"error.generic.rate_limited": "Слишком попыток, попробуйте ещё раз позже.",
"error.register.flows_loading_error": "Не удалось загрузить методы регистрации.",
"error.register.unsupported": "Регистрация на этом домашнем сервере не поддерживается.",
"register.login_tip": "Уже есть аккаунт?",
"register.login_link": "Войти",
"password_reset.success": "Пароль был сброшен.",
"password_reset.login": "Войти",
"password_reset.tip": "Сервер {0} отправит Вам письмо для сброса пароля.",
"form.email": "Email",
"password_reset.new": "Новый пароль",
"password_reset.confirm": "Подтвердить пароль",
"error.password_reset": "Не удалось сбросить пароль.",
"password_reset.button": "Сбросить пароль",
"form.reset_password": "Сброс пароля",
"password_reset.login_tip": "Вспомнили пароль?",
"password_reset.login_link": "Войти",
"footer.about": "О приложении",
"footer.mx_room": "Комната Matrix",
"footer.mx_powered": "На базе [matrix]",
"form.homeserver": "Домашний сервер",
"form.homeserver_loading": "Ищем домашний сервер...",
"form.homeserver_error": "Не удалось найти домашний сервер.",
"error.hs_failed_to_connect": "Не удалось подключиться. {0} недоступен для использования.",
"error.hs_invalid_base": "Не удалось подключиться. Базовый URL домашнего сервера неверен.",
"auth.connecting": "Подключение к {0}...",
"error.hs_unavailable": "Не удалось подключиться. Домашний сервер недоступен или не существует.",
"auth.loading_flow": "Загрузка методов авторизации...",
"error.loading_flow": "Не удалось загрузить информацию метода авторизации",
"picker.hs_list": "Список серверов",
"sso.continue_with": "Продолжить с {0}",
"chats.mark_as_read": "Отметить как прочитанное",
"direct_menu.title": "Личные сообщения",
"direct_menu.empty": "Нет личных сообщений",
"direct_menu.empty.2": "У Вас ещё нет личных сообщений.",
"direct_menu.empty.start_new": "Начать ЛС",
"direct_menu.new": "Новый чат",
"direct_menu.chats": "Чаты",
"explore.add_server": "Добавить сервер",
"explore.add_server.desc": "Добавить сервер для поиска сообществ.",
"input.explore.server_name": "Имя сервера",
"error.explore.load_rooms": "Не удалось загрузить комнаты.",
"explore.view": "Показать",
"explore.add_server_btn": "Добавить сервер",
"explore.title": "Поиск сообществ",
"explore.featured": "Избранное",
"explore.servers": "Сервера",
"explore.featured.title": "Выбрано клиентом",
"explore.featured.subtitle": "Поиск сообществ, выбранных провайдером клиента",
"explore.featured_spaces": "Избранные пространства",
"explore.featured_rooms": "Избранные комнаты",
"explore.no_featured": "Провайдер клиента не выбрал комнаты/пространства.",
"explore.server.room_type_filter.all": "Все",
"explore.server.room_type_filter.spaces": "Пространства",
"explore.server.room_type_filter.rooms": "Комнаты",
"explore.server.search": "Поиск",
"placeholder.explore.server.search": "Искать по ключевому слову",
"btn.clear_search": "Очистить",
"btn.submit_search": "Ввод",
"explore.server.protocols": "Протоколы",
"presets": "Пресеты",
"count.custom_limit": "Указать лимит",
"aria.per_page_item_limit": "Лимит предметов на страницу",
"count.set_limit": "Применить",
"count.page_limit": "Лимит: {0}",
"search.results": "Результаты по \"{0}\"",
"explore.popular_communities": "Популярные сообщества",
"btn.prev_page": "Предыдущая страница",
"btn.next_page": "Следующая страница",
"btn.prev": "Пред.",
"btn.next": "След.",
"btn.jump_to_page": "Перейти к странице",
"explore.server.no_communities": "Сообщества не найдены!",
"home.title": "Домашняя страница",
"home.empty": "Нет комнат",
"home.empty.2": "У Вас ещё нет комнат.",
"home.empty.new": "Создать",
"home.empty.explore": "Поиск комнат",
"home.new_room": "Создать чат",
"home.join_via_address": "Войти по адресу",
"home.search_messages": "Поиск сообщений",
"home.rooms": "Комнаты",
"msg_search.title": "Поиск сообщений",
"inbox.invites": "Приглашения",
"inbox.invites.title": "Приглашения",
"inbox.title": "Входящие",
"inbox.notifications": "Уведомления",
"inbox.invites.by": "Приглашён {0}",
"btn.decline": "Отклонить",
"btn.accept": "Принять",
"inbox.invites.dm": "Личные сообщения",
"inbox.invites.space": "Пространства",
"inbox.invites.room": "Комнаты",
"inbox.invites.empty": "Нет приглашений",
"inbox.invites.empty.2": "У Вас ещё нет приглашений.",
"unknown_event": "Событие типа {0}",
"notifications.room_tombstone": "Конец беседы: {0}",
"notifications.mark_as_read": "Отметить как прочитанное",
"notifications.open_msg": "Открыть",
"notifications.title": "Уведомления",
"notifications.filter": "Фильтр",
"notifications.filter.all": "Все",
"notifications.filter.highlighted": "Упоминания",
"scroll_to_top": "На верх",
"notifications.empty": "Нет уведомлений",
"notifications.empty.2": "У Вас ещё нет уведомлений.",
"btn.unpin": "Открепить",
"btn.pin": "Закрепить",
"space.action.invite": "Пригласить",
"space.action.copy_link": "Скопировать ссылку",
"space.action.settings": "Настройки",
"space.action.leave": "Покинуть",
"sidebar.tooltip.users": "Пользователи",
"space.lobby": "Лобби",
"rooms": "Комнаты",
"nav.create_space": "Создать пространство",
"connecting": "Подключение к серверу",
"error.hs_connect": "Не удалось подключиться к домашнему серверу. Возможно, сервер недоступен или произошла проблема с Вашим сетевым подключением.",
"btn.retry": "Попробовать снова",
"btn.continue": "Продолжить",
"welcome": "Добро пожаловать в <clientName>!",
"welcome.2": "Форк Cinny с прикольными фишками.",
"btn.source_code": "Исходный код",
"btn.sponsor": "Поддержать",
"error.config_load": "Не удалось загрузить конфигурацию клиента.",
"error.unsupported_browser": "Браузер не поддерживается",
"error.indexed_db_unsupported": "Ваш браузер не поддерживает IndexedDB. Это приложение использует IndexedDB чтобы хранить данные сессии локально. Пожалуйста, проверьте, что Ваш браузер поддерживает IndexedDB.",
"qna.indexeddb": "Что такое IndexedDB?",
"emojiboard.stickers": "Стикеры",
"emojiboard.emojis": "Эмодзи",
"emojiboard.personal_pack": "Свой набор",
"emojiboard.unknown_pack": "Неизвестный набор",
"emojigroup.unknown_name": "Неизвестно",
"emojiboard.no_sticker_packs": "Нет наборов стикеров",
"emojiboard.no_sticker_packs.2": "Добавьте стикеры в настройках пользователя, комнаты или пространства",
"emojiboard.search_or_text_reaction": "Поиск эмодзи или отправить текстовую реакцию",
"emojiboard.search": "Поиск",
"emojiboard.react": "Отправить реакцию",
"emojiboard.results": "Результаты поиска",
"emojiboard.no_results": "Ничего не найдено",
"egid_people": "Смайлики и люди",
"egid_nature": "Животные и природа",
"egid_food": "Еда и напитки",
"egid_activity": "Активности",
"egid_travel": "Путешествие и места",
"egid_object": "Объекты",
"egid_symbols": "Символы",
"egid_flag": "Флаги",
"event_readers.seen_by": "Прочитано",
"aria.zoom_out": "Уменьшить",
"aria.zoom_in": "Увеличить",
"btn.download": "Скачать",
"leaveroom.title": "Покинуть комнату",
"leaveroom.text": "Вы уверены что хотите покинуть эту комнату?",
"leaveroom.text.2": "Вы уверены что хотите выйти из {0}?",
"error.leaveroom": "Не удалось покинуть комнату! {0}",
"leaveroom.leaving": "Выходим...",
"btn.leave": "Покинуть",
"leavespace.title": "Покинуть пространство",
"leavespace.text": "Вы уверены что хотите покинуть это пространство?",
"error.leavespace": "Не удалось покинуть пространство! {0}",
"leavespace.leaving": "Выходим...",
"msg.redacted": "Сообщение удалено",
"msg.redacted.reason": "Сообщение удалено: {0}",
"msg.failed.load": "Ошибка при загрузке сообщения",
"msg.failed.decrypt": "Ошибка при расшифровке сообщения",
"msg.not_decrypted": "Расшифровка этого сообщения может занять некоторое время",
"msg.unsupported": "Неподдерживаемое сообщение",
"msg.broken": "Сломанное сообщение",
"msg.empty": "Пустое сообщение",
"msg.edited": " (изменено)",
"msg.file.failed": "Не удалось загрузить файл",
"btn.open_file": "Открыть файл",
"btn.open_pdf": "Открыть PDF",
"btn.retry_download": "Скачать ещё раз ({0})",
"btn.download_size": "Скачать ({0})",
"btn.view": "Показать",
"msg.image.failed": "Не удалось загрузить картинку",
"msg.thumbnail.failed": "Не удалось загрузить предпросмотр",
"btn.watch": "Смотреть",
"msg.video.failed": "Не удалось загрузить видео",
"generic.delimiter": ",",
"generic.and": " и ",
"generic.unknown": "[Неизвестно]",
"generic.others": "{0} других",
"reaction.reacted_with": " отреагировал ",
"time.yesterday": "Вчера {0}",
"error.pdfviewer": "Не удалось загрузить PDF",
"aria.page_number": "Номер страницы",
"btn.cancel": "Отмена",
"generic.space": "Пространство",
"generic.room": "Комната",
"generic.member_count": "{0} участников",
"room_card.joining": "Входим...",
"btn.join": "Войти",
"btn.error_details": "Детали ошибки",
"error.join.unknown": "Не удалось войти.",
"room_intro.1": "Это начало беседы.",
"room_intro.2": "Создано {0} {1}.",
"btn.invite": "Пригласить",
"btn.old_room.open": "Открыть старую комнату",
"btn.old_room.join": "Присоединиться к старой комнате",
"btn.copy_all": "Скопировать всё",
"error.register.unknown": "Не удалось зарегистрироваться.",
"btn.verify_email": "Подтвердить почту",
"email_stage.verifying": "Подтверждаем почту...",
"error.verify_email.unknown": "Не удалось подтвердить почту.",
"error.email_stage": "Ошибка подтверждения",
"email_stage.sent": "Ссылка подтверждения отправлена",
"email_stage.sent.2": "Проверьте Вашу почту ({0}) и следуйте инструкциям. Не забудьте проверить папку Спам.",
"recaptcha_stage.error": "Неверная капча",
"recaptcha_stage.error.2": "Была получена неверная капча.",
"recaptcha_stage.tip": "Нажмите кнопку ниже чтобы продолжить.",
"form.reg_token": "Токен регистрации",
"error.reg_token.invalid": "Неверный токен регистрации.",
"error.reg_token.1.title": "Требуется токен",
"error.reg_token.1.msg": "Введите токен регистрации, полученный от администратора сервера.",
"error.tos": "Не удалось принять Условия использования.",
"upload_board.title": "Файлы",
"btn.ub_send": "Отправить",
"error.upload": "Не удалось загрузить",
"btn.ub_remove": "Убрать",
"btn.ub_remove_all": "Убрать всё",
"aria.retry_upload": "Повторить попытку",
"aria.cancel_upload": "Отменить",
"error.load_auth_flow": "Не удалось загрузить метод авторизации!",
"error.missing_auth_flow": "Нет метода авторизации!",
"generic.step": "Шаг {0}",
"generic.step.2": "Шаг {0} из {1}",
"btn.exit": "Выход",
"btn.space_lobby.suggest": "Отметить как рекомендованное",
"btn.space_lobby.unsuggest": "Отметить как нерекомендованное",
"btn.space_lobby.remove_room": "Убрать",
"btn.space_lobby.invite": "Пригласить",
"btn.space_lobby.settings": "Настройки",
"btn.space.pin": "Закрепить",
"btn.space.unpin": "Открепить",
"aria.scroll_to_top": "Прокрутить вверх",
"generic.members": "Участники",
"generic.more_options": "Больше",
"space_room.suggested": "Рекомендуется",
"space_room.private": "Приватный",
"space_item.suggested": "Рекомендуется",
"space_item.private": "Приватный",
"space_item.rooms": "Комнаты",
"btn.space.new_room": "Новая комната",
"btn.space.existing_room": "Существующая комната",
"btn.space.new_space": "Новое пространство",
"btn.space.existing_space": "Существующее пространство",
"btn.space.add_room": "Добавить комнату",
"btn.space.add_space": "Добавить пространство",
"msg_search.title.2": "Поиск сообщений",
"msg_search.subtitle.2": "Найти полезные сообщения по ключевым словам.",
"generic.no_results": "Нет результатов для {0}",
"generic.no_results.2": "Нет результатов",
"generic.results": "Результаты для \"{0}\"",
"generic.sort_by": "Сортировать по",
"sort.recent": "Недавнее",
"sort.relevance": "Актуальное",
"form.search.query": "Поиск",
"generic.results.room": "Комнаты по запросу \"{0}\"",
"generic.no_matches": "Нет соответствий",
"generic.no_matches.2": "Нет соответствий по запросу \"{0}\"",
"btn.save": "Сохранить",
"btn.save.2": "Сохранить ({0})",
"btn.deselect_all": "Отменить выбор (все)",
"search_filters.select_rooms": "Выбрать комнаты",
"search_filters.global": "Все",
"search_input.header": "Поиск",
"placeholder.search_input": "Поиск по ключевому слову",
"btn.search.clear": "Очистить",
"search_input.enter": "Ввод",
"btn.search_result.open": "Открыть",
"msg_menu.view_reactions": "Реакции",
"msg_menu.read_receipts": "Отчёты о прочтении",
"msg_menu.view_source": "Исходный код",
"msg_menu.recover": "Восстановить",
"error.recover": "Ваш сервер не реализовывает Synapse Admin API или Вы не являетесь администратором",
"msg_menu.copy_link": "Скопировать ссылку",
"msg_redact.title": "Удалить сообщение",
"msg_redact.subtitle": "Это действие необратимо! Вы уверены что хотите удалить это сообщение?",
"msg_redact.reason.1": "Причина",
"msg_redact.reason.2": "(необязательно)",
"error.redact_msg": "Не удалось удалить сообщение! Попробуйте ещё раз.",
"msg_redact.processing": "Удаляем...",
"btn.msg_redact": "Удалить",
"msg_menu.redact": "Удалить",
"msg_report.title": "Пожаловаться на сообщение",
"msg_report.subtitle": "Сообщить об этом сообщении администраторам сервера, что может осведомить нужных людей.",
"msg_report.reason": "Причина",
"error.msg_report": "Не удалось пожаловаться! Попробуйте ещё раз.",
"success.msg_report": "Жалоба отправлена.",
"msg_report.processing": "Отправляем жалобу...",
"btn.msg_report": "Пожаловаться",
"msg.too_many_buttons": "Слишком много кнопок",
"msg_menu.end_poll": "Завершить опрос",
"msg_menu.add_reaction": "Отреагировать",
"msg_menu.reply": "Ответить",
"msg_menu.edit": "Изменить",
"placeholder.msg_edit": "Изменить сообщение...",
"btn.msg_edit.save": "Изменить",
"reaction_viewer.reacted_with": "Отреагировал с :{0}:",
"autocomplete.commands.title": "Команды",
"autocomplete.commands.tip": "Начните Ваше сообщение командой",
"members_drawer.joined": "Вошедшие",
"members_drawer.invited": "Приглашённые",
"members_drawer.left": "Вышедшие",
"members_drawer.kicked": "Выгнанные",
"members_drawer.banned": "Заблокированные",
"sort.a_to_z": "А до Я",
"sort.z_to_a": "Я до А",
"sort.newest": "Новые",
"sort.oldest": "Старые",
"tooltip.close": "Закрыть",
"btn.close": "Закрыть",
"placeholder.search_name": "Поиск по имени...",
"generic.result_count": "{0} результат(ов)",
"members_drawer.no_members.joined": "Нет вошедших участников",
"members_drawer.no_members.invited": "Нет приглашённых участников",
"members_drawer.no_members.left": "Нет вышедших участников",
"members_drawer.no_members.kicked": "Нет выгнанных участников",
"members_drawer.no_members.banned": "Нет заблокированных участников",
"room_input.drop_files": "Отправить файлы в {0}",
"room_input.drop_files.this_room": "эту комнату",
"room_input.drop_files.2": "Перетащите файлы сюда или нажмите для окна выбора",
"placeholder.room_input": "Отправить сообщение...",
"event.room_name": "{0} изменил(а) название комнаты",
"event.room_topic": "{0} изменил(а) тему комнаты",
"event.room_avatar": "{0} изменил(а) картинку комнаты",
"timeline.unknown_state_event": "{0} отправил(а) событие состояния {1}",
"timeline.unknown_event": "{0} отправил(а) событие {1}",
"timeline.new_messages_divider": "Новые",
"timeline.today_divider": "Сегодня",
"timeline.yesterday_divider": "Вчера",
"btn.timeline.jump_to_unread": "Перейти к непрочитанному",
"btn.timeline.mark_as_read": "Отметить как прочитанное",
"btn.timeline.jump_to_latest": "Перейти к последним сообщениям",
"room_tombstone.default_reason": "Эта комната была заменена и больше не активна.",
"error.tombstone.join_failed": "Не удалось войти в новую комнату",
"btn.room_tombstone.new_room": "Открыть новую комнату",
"btn.room_tombstone.join_room": "Войти в новую комнату",
"following.one": "{0} участвует в беседе.",
"following.two": "{0} и {1} участвуют в беседе.",
"following.three": "{0}, {1} и {2} участвуют в беседе.",
"following.more": "{0}, {1}, {2} и {3} участвуют в беседе.",
"typing.one": "{0} печатает...",
"typing.two": "{0} и {1} печатают...",
"typing.three": "{0}, {1} и {2} печатают...",
"typing.more": "{0}, {1}, {2} и {3} печатают...",
"room_header.mark_as_read": "Отметить как прочитанное",
"room_header.invite": "Пригласить",
"room_header.copy_link": "Скопировать ссылку",
"room_header.settings": "Настройки",
"room_header.leave": "Покинуть",
"tooltip.search": "Поиск",
"tooltip.members": "Участники",
"tooltip.more_options": "Больше",
"msg_preview.redacted": "[Сообщение удалено]",
"msg_preview.failed": "[Не удалось загрузить предпросмотр]",
"rename_pack.title": "Переименовать набор",
"btn.rename_pack": "Переименовать",
"delete_pack.title": "Удалить набор",
"delete_pack.desc": "Вы уверены что хотите удалить \"{0}\"?",
"btn.delete_pack": "Удалить",
"image_pack.image": "Картинка",
"image_pack.shortcode": "Название",
"image_pack.usage": "Использование",
"generic.view_less": "Меньше",
"generic.view_more": "Больше",
"generic.view_more.count": "Показать ещё {0}",
"image_pack.global_use.1": "Использовать везде",
"image_pack.global_use.2": "Использовать этот набор в любых чатах.",
"image_pack.global": "Глобальные наборы",
"image_pack.no_global": "Нет глобальных наборов",
"image.usage.emoji": "Эмодзи",
"image.usage.sticker": "Стикер",
"image.usage.both": "Оба",
"label.name": "Имя",
"label.attribution": "Авторство",
"tooltip.edit": "Изменить",
"image_pack.pack_usage": "Использование набора",
"tooltip.remove_file": "Удалить файл",
"btn.import_image": "Импорт",
"generic.uploading": "Загружаем...",
"btn.upload": "Загрузить",
"img_upload.upload": "Загрузить",
"img_upload.cancel": "Отмена",
"img_upload.remove": "Убрать",
"placeholder.confirm_password": "Подтвердите пароль",
"placeholder.password": "Пароль",
"btn.export_keys": "Экспорт",
"error.export_keys": "Не удалось экспортировать ключи. Попробуйте ещё раз.",
"success.export_keys": "Ключи экспортированы.",
"error.passwords_didnt_match": "Пароли не совпадают.",
"export_keys.getting": "Получение ключей...",
"export_keys.encrypting": "Шифрование ключей...",
"import_keys.decrypting": "Расшифровка файла...",
"import_keys.msg_decrypting": "Расшифровка сообщений...",
"success.import_keys": "Ключи импортированы.",
"error.import_keys": "Не удалось расшифровать ключи. Попробуйте ещё раз.",
"btn.import_keys": "Импорт",
"btn.import_keys.decrypt": "Расшифровать",
"pl_selector.presets": "Уровни прав",
"power_level.admin": "Админ - 100",
"power_level.mod": "Модератор - 50",
"power_level.member": "Участник - 0",
"error.invalid_characters.a-zA-Z0-9_-": "Неверные символы. Используйте только латинские буквы, цифры, подчёркивания и тире.",
"room_aliases.validating": "Проверяем {0}...",
"room_aliases.available": "{0} доступен.",
"room_aliases.used": "{0} уже занят.",
"room_aliases.deleting": "Удаляем...",
"btn.room_aliases.delete": "Удалить",
"btn.room_aliases.main": "Указать как главный",
"btn.room_aliases.publish": "Опубликовать",
"btn.room_aliases.unpublish": "Убрать",
"room_aliases.main": "Главный",
"room_aliases.publish": "Опубликовать {0} в каталог комнат {1}?",
"room_aliases.publish.room": "эту комнату",
"room_aliases.publish.space": "это пространство",
"room_aliases.published": "Опубликованные адреса",
"room_aliases.no_published": "Нет опубликованных адресов",
"room_aliases.no_main_address": "Нет главного адреса. Выберите его ниже.",
"room_aliases.message": "Опубликованные адреса могут быть использованы кем угодно на любых серверах чтобы присоединиться к {0}. Чтобы опубликовать адрес, он должен быть локальным.",
"room_aliases.message.room": "этой комнате",
"room_aliases.message.space": "этому пространству",
"room_aliases.local": "Локальные адреса",
"room_aliases.no_local": "Нет локальных адресов",
"room_aliases.message.2": "Укажите локальные адреса, чтобы пользователи с этого сервера могли найти {0}.",
"room_aliases.message.room.2": "эту комнату",
"room_aliases.message.space.2": "это пространство",
"room_aliases.add_local": "Добавить локальный адрес",
"btn.add": "Добавить",
"btn.show_local_address": "Добавить / показать локальные адреса",
"btn.hide_local_address": "Скрыть локальные адреса",
"room_emojis.create": "Создать набор",
"leavespace.text.2": "Вы уверены что хотите покинуть пространство \"{0}\"?",
"placeholder.create_pack_name": "Название набора",
"btn.room_emojis.create_pack": "Создать набор",
"room_emojis.empty": "Ещё нет наборов.",
"room_encryption.confirm.1": "Не рекомендуется включать шифрование в общедоступных комнатах. Кто угодно может найти и присоединиться к ним, а значит и читать в них сообщения.",
"room_encryption.confirm.2": "Шифрование комнаты не может быть отключено. Зашифрованные сообщения не видны серверу, только участникам комнаты. Это ломает большинство мостов и ботов.",
"dialog.room_encryption.title": "Включить шифрование",
"btn.dialog.room_encryption.1": "Продолжить",
"btn.dialog.room_encryption.2": "Включить",
"room_encryption.setting.title": "Включить шифрование комнаты",
"room_encryption.setting.tip": "Шифрование не может быть выключено в будущем.",
"history_visibility.anyone": "Все (включая гостей)",
"history_visibility.members": "Участники (все сообщения)",
"history_visibility.invited": "Участники (сообщения после приглашения)",
"history_visibility.joined": "Участники (сообщения после входа)",
"history_visibility.tip": "Изменение видимости истории применяется только к будущим сообщениям.",
"room_members.search_title": "Поиск участников",
"placeholder.search_room_members": "Поиск по имени",
"room_members.1": "{0} участников",
"room_members.found": "Найдено {0} участников",
"room_members.joined": "Вошедшие",
"room_members.invited": "Приглашённые",
"room_members.banned": "Забаненные",
"room_members.empty": "Не найдено участников для показа",
"room_notifications.global": "По умолчанию",
"room_notifications.all": "Все сообщения",
"room_notifications.mentions": "Упоминания и ключевые слова",
"room_notifications.mute": "Отключить",
"perms.default.title": "Уровень прав по умолчанию",
"perms.default.desc": "Уровень прав по умолчанию для новых участников.",
"perms.send.title": "Отправлять сообщения",
"perms.send.desc": "Минимальный уровень прав для отправки сообщений.",
"perms.react.title": "Отправлять реакции",
"perms.react.desc": "Минимальный уровень прав для отправки реакций.",
"perms.redact.title": "Удалять сообщения",
"perms.redact.desc": "Минимальный уровень прав для удаления сообщений.",
"perms.ping_room.title": "Упомянуть комнату (@room)",
"perms.ping_room.desc": "Минимальный уровень прав для упоминания комнаты (@room).",
"perms.manage_rooms.title": "Управлять комнатами",
"perms.manage_rooms.desc": "Минимальный уровень прав для изменения комнат в пространстве.",
"perms.invite.title": "Приглашать участников",
"perms.invite.desc": "Минимальный уровень прав для приглашения участников.",
"perms.kick.title": "Выгонять участников",
"perms.kick.desc": "Минимальный уровень прав чтобы выгнать участника.",
"perms.ban.title": "Банить участников",
"perms.ban.desc": "Минимальный уровень прав чтобы банить участников.",
"perms.avatar.title": "Изменить аватарку комнаты",
"perms.avatar.desc": "Минимальный уровень прав чтобы изменить аватарку комнаты.",
"perms.name.title": "Изменить название комнаты",
"perms.name.desc": "Минимальный уровень прав чтобы изменить название комнаты/пространства.",
"perms.topic.title": "Изменить тему комнаты",
"perms.topic.desc": "Минимальный уровень прав чтобы изменить тему комнаты/пространства.",
"perms.state.title": "Изменить настройки",
"perms.state.desc": "Минимальный уровень прав чтобы изменить настройки комнаты.",
"perms.canonical.title": "Изменить адреса комнаты",
"perms.canonical.desc": "Минимальный уровень прав чтобы изменить адреса комнаты/пространства.",
"perms.power_levels.title": "Изменить права",
"perms.power_levels.desc": "Минимальный уровень прав чтобы изменить права комнаты/пространства.",
"perms.encryption.title": "Включить шифрование",
"perms.encryption.desc": "Минимальный уровень прав чтобы включить шифрование.",
"perms.history.title": "Изменить видимость истории",
"perms.history.desc": "Минимальный уровень прав чтобы изменить видимость истории сообщений.",
"perms.tombstone.title": "Обновить комнату",
"perms.tombstone.desc": "Минимальный уровень прав чтобы обновить комнату.",
"perms.pinned.title": "Закреплять сообщения",
"perms.pinned.desc": "Минимальный уровень прав для закрепления сообщений.",
"perms.server_acl.title": "Изменить заблокированные сервера",
"perms.server_acl.desc": "Минимальный уровень прав чтобы изменить заблокированные сервера.",
"perms.widgets.title": "Изменить виджеты комнаты",
"perms.widgets.desc": "Минимальный уровень прав чтобы изменить виджеты комнаты.",
"room_perms.general": "Основные права",
"room_perms.members": "Управление участниками",
"room_perms.room_profile": "Управление профилем комнаты",
"room_perms.settings": "Права настроек",
"room_perms.other": "Другие права",
"space_perms.space_profile": "Управление профилем пространства",
"generic.pl_member": "Участник",
"room_profile.saving_name": "Сохранение названия комнаты...",
"room_profile.saving_topic": "Сохранение темы комнаты...",
"room_profile.saved": "Профиль комнаты сохранён",
"error.room_profile": "Не удалось сохранить профиль комнаты.",
"confirm.remove_room_avatar.title": "Удалить аватарку комнаты",
"confirm.remove_room_avatar.desc": "Вы уверены что хотите удалить аватарку комнаты?",
"btn.remove_room_avatar": "Удалить",
"room_profile.only": "У Вас есть права только для изменения {1} {0}.",
"room_profile.only.room": "этой комнаты",
"room_profile.only.space": "этого пространства",
"room_profile.only.name": "названия",
"room_profile.only.topic": "темы",
"btn.room_profile.save": "Сохранить",
"room_tile.invited": "Приглашение от {0} в {1}{2}",
"room_tile.members": " • {0} участников",
"room_tile.info": "{0}{1}",
"room_visibility.invite": "По приглашению",
"room_visibility.restricted": "Участники пространства",
"room_visibility.restricted.unsupported": "Участники пространства (требуется обновление комнаты)",
"room_visibility.public": "Общедоступный",
"space_add_existing.adding": "Добавление {0} предметов...",
"placeholder.search_room": "Поиск комнаты",
"btn.space_add_existing.add": "Добавить",
"space_add_existing.item_selected": "{0} выбрано",
"space_add_existing.tip": " — добавить существующие {0}",
"space_add_existing.tip.rooms": "комнаты",
"space_add_existing.tip.spaces": "пространства",
"error.create_room.invalid_characters": "Ошибка: Запрещённые символы в адресе комнаты",
"error.create_room.address_already_used": "Ошибка: Этот адрес уже используется",
"create_room.join_rule.short.private": "По приглашению",
"create_room.join_rule.short.restricted": "Участники пространства",
"create_room.join_rule.short.public": "Общедоступный",
"create_room.join_rule.private": "По приглашению",
"create_room.join_rule.restricted": "Участники пространства",
"create_room.join_rule.public": "Общедоступный",
"create_room.join_rule": "Видимость комнаты (кто может войти)",
"create_room.join_rule.title": "Видимость комнаты",
"create_room.join_rule.tip": "Выберите, кто может войти в {0}",
"create_room.join_rule.tip.room": "эту комнату",
"create_room.join_rule.tip.space": "это пространство",
"create_room.address.room": "Адрес комнаты",
"create_room.address.space": "Адрес пространства",
"error.create_room.address_in_use": "{0} уже используется",
"create_room.encrypt.title": "Сквозное шифрование",
"create_room.encrypt.desc": "Вы не сможете отключить это позже. Мосты и многие боты не будут работать.",
"create_room.your_pl.title": "Выберите Ваш уровень прав",
"create_room.your_pl.desc": "Админ - 100. Основатель - 101. Гоку - 9001.",
"create_room.your_pl.admin": "Админ",
"create_room.your_pl.founder": "Основатель",
"create_room.your_pl.goku": "Гоку",
"create_room.topic": "Тема (необязательно)",
"create_room.name": "Название {0}",
"create_room.name.room": "комнаты",
"create_room.name.space": "пространства",
"btn.create_room": "Создать",
"create_room.creating": "Создаём...",
"emoji_verification.waiting": "Ожидание ответа от другого устройства...",
"emoji_verification.tip": "Подтвердите что эмодзи ниже видны на обеих устройствах, в том же порядке:",
"btn.emoji_verification.they_match": "Они одинаковые",
"btn.emoji_verification.no_match": "Они разные",
"emoji_verification.outgoing": "Примите запрос на другом устройстве.",
"emoji_verification.ingoing": "Нажмите кнопку ниже чтобы начать процесс подтверждения.",
"btn.emoji_verification.accept": "Принять",
"emoji_verification.title": "Подтверждение по эмодзи",
"error.invite.not_found": "{0} не найден!",
"btn.open": "Открыть",
"invite.joined": "Присоединился",
"invite.invited": "Приглашён",
"invite.banned": "Забанен",
"btn.dm": "Написать сообщение",
"invite.title": "Пригласить в {0}",
"invite.dm": "Личное сообщение",
"btn.invite.search": "Поиск",
"label.name_or_id": "Имя или Matrix ID",
"invite.searching": "Ищем пользователя \"{0}\"...",
"invite.result": "Результат поиска для \"{0}\"",
"error.join_alias.invalid": "Неверный адрес.",
"join_alias.looking": "Поиск комнаты по адресу...",
"join_alias.joining": "Вход в {0}...",
"error.join_alias.not_found": "Не удалось найти комнату или пространство по адресу {0}. Комната/пространство не существует, либо имеет доступ по приглашению.",
"error.join_alias.unable_to_join": "Не удалось войти в {0}. Комната/пространство не существует, либо имеет доступ по приглашению.",
"btn.join_alias.join": "Войти",
"join_alias.title": "Войти по адресу",
"profile_editor.remove_avatar.title": "Удалить аватарку",
"profile_editor.remove_avatar.desc": "Вы уверены что хотите удалить аватарку?",
"btn.profile_editor.remove_avatar": "Удалить",
"profile_editor.displayname": "Ваше отображаемое имя",
"btn.profile_editor.save_name": "Сохранить",
"label.profile_viewer.kick_reason": "Причина",
"label.profile_viewer.ban_reason": "Причина",
"btn.profile_viewer.kick": "Выгнать",
"btn.profile_viewer.ban": "Забанить",
"session_info.loading": "Загрузка устройств...",
"session_info.none": "Нет устройств.",
"session_info.item": "Показать {0} устройств(а)",
"btn.profile_footer.dm": "Написать",
"profile_footer.dm.creating": "Создание ЛС...",
"btn.profile_footer.unban": "Разбанить",
"btn.profile_footer.disinviting": "Удаление приглашения...",
"btn.profile_footer.disinvite": "Снять приглашение",
"btn.profile_footer.unignoring": "Загрузка...",
"btn.profile_footer.unignore": "Перестать игнорировать",
"btn.profile_footer.ignoring": "Загрузка...",
"btn.profile_footer.ignore": "Игнорировать",
"btn.profile_footer.inviting": "Приглашение...",
"btn.profile_footer.invite": "Пригласить",
"shared_pl_warning": "Вы не сможете отменить это изменение, так-как у Вас будет одинаковый уровень прав с этим пользователем. Вы уверены?",
"self_demote_warning": "Вы не сможете отменить это изменение, так-как понижаете свой уровень прав. Вы уверены?",
"profile_viewer.change_power_level.title": "Изменить уровень прав",
"btn.profile_viewer.change_pl": "Изменить",
"profile_viewer.power_level": "Уровень прав",
"room_settings.general": "Основные",
"room_settings.members": "Участники",
"room_settings.emojis": "Эмодзи",
"room_settings.permissions": "Права",
"room_settings.security": "Безопасность",
"room_settings.menuheader.notification": "Уведомления (только для Вас)",
"room_settings.menuheader.visibility": "Видимость комнаты (кто может войти)",
"space_settings.visibility": "Видимость комнаты (кто может войти)",
"space_settings.addresses": "Адреса пространства",
"room_settings.menuheader.addresses": "Адреса комнаты",
"room_settings.menuheader.encryption": "Шифрование",
"room_settings.menuheader.history": "Видимость истории сообщений",
"placeholder.search_chats": "Поиск",
"search.tip": "Введите # для комнат, @ для ЛС и * для пространств. Горячая клавиша: Ctrl + K",
"error.auth_request": "Неверный пароль.",
"error.auth_request.failed": "Запрос не удался!",
"label.auth_request.password": "Пароль аккаунта",
"btn.auth_request.continue": "Продолжить",
"error.crosssigning": "Не удалось настроить перекрёстную подпись. Попробуйте ещё раз.",
"error.crosssigning.title": "Настроить перекрёстную подпись",
"btn.copy": "Скопировать",
"crosssigning.tip": "Сохраните этот ключ в безопасном месте. Обращайтесь с ним как с паролем.",
"crosssigning.key.title": "Ключ безопасности",
"crosssigning.title": "Настроить перекрёстную подпись",
"error.password.12345678": "Как насчёт 87654321?",
"error.password.87654321": "Чел с огнём играет",
"error.password.8127.ns": "Пароль должен содержать от 8 до 127 символов (без пробелов).",
"error.password.dontmatch": "Пароли не совпадают.",
"crosssigning.intro": "Мы сгенерируем {0}, чтобы Вы смогли управлять резервной копией сообщений и подтверждать устройства.",
"crosssigning.intro.security_key": "Ключ безопасности",
"btn.crosssigning.generate": "Создать ключ",
"generic.OR": "ИЛИ",
"crosssigning.intro.2": "Как вариант, Вы можете использовать Фразу безопасности, чтобы не запоминать длинный ключ. Также Вы можете сохранить ключ на всякий случай.",
"label.security_phrase": "Секретная фраза",
"label.security_phrase.confirm": "Подтвердите фразу",
"btn.set_phrase_and_generate_key": "Указать фразу и создать ключ",
"reset_cross_signing.title": "Сбросить перекрёстную подпись",
"reset_cross_signing.warning": "Сброс перекрёстной подписи необратим.",
"reset_cross_signing.warning.2": "Все, с кем Вы подтвердили устройства, увидят предупреждение и Ваша резервная копия сообщений будет потеряна. Вам точно не надо это делать, если Вы не потеряли Ключ безопасности или Секретную фразу, а также каждую сессию с поддержкой перекрёстной подписи.",
"btn.reset_cross_signing": "Сбросить",
"btn.setup_cross_signing": "Настроить",
"cross_signing_tile.title": "Перекрёстная подпись",
"cross_signing_tile.desc": "Настройте для подтверждения и управления всеми устройствами. Также требуется для резервной копии сообщений.",
"btn.save_device_name": "Сохранить",
"label.session_name": "Имя устройства",
"text.edit_session_name": "Изменить имя устройства",
"loading_sessions": "Загрузка устройств...",
"device_manage.logout.title": "Выйти из {0}",
"device_manage.logout.warning": "Вы удаляете устройство \"{0}\" из аккаунта.",
"btn.logout": "Выйти",
"text.session.current": "Текущее устройство",
"tooltip.rename_session": "Переименовать устройство",
"tooltip.remove_session": "Удалить устройство",
"device_manage.last_activity": "Последняя активность {0}{1}",
"device_manage.last_activity.ip": " с IP {0}",
"text.session.unverified": "Неподтверждённые устройства",
"device_manage.verify.title": "Подтвердите это устройство с помощью Ключа безопасности/секретной фразы. Как вариант, Вы можете начать подтверждение по эмодзи.",
"device_manage.tip": "Подтвердите другие устройства или выйдите из незнакомых.",
"device_manage.crosssigning_tip": "Настроить перекрёстную подпись на случай, если Вы потеряете все сессии.",
"text.session.no_unverified": "Нет неподтверждённых устройств",
"text.session.no_encryption": "Устройства без шифрования",
"text.session.no_verified": "Нет подтверждённых устройств",
"device_manage.name_warning": "Названия устройств видны всем, поэтому не пишите тут что-то секретное.",
"key_backup.creating": "Создание резервной копии...",
"key_backup.created": "Резервная копия создана",
"error.key_backup": "Не удалось создать резервную копию",
"key_backup.restoring": "Восстановление резервной копии... ({0}/{1})",
"key_backup.success": "Резервная копия успешно восстановлена ({0} ключей из {1}).",
"error.key_backup.invalid_key": "Не удалось восстановить резервную копию. Неверный ключ!",
"error.key_backup.unknown": "Не удалось восстановить резервную копию.",
"key_backup.restoring.2": "Восстановление резервной копии...",
"key_backup.delete.warning": "Удаление резервной копии необратимо.",
"key_backup.delete.warning.2": "Все зашифрованные ключи, сохранённые на сервере, будут удалены.",
"btn.key_backup.delete": "Удалить",
"key_backup.create.title": "Создать резервную копию ключей",
"key_backup.restore.title": "Восстановить резервную копию ключей",
"key_backup.delete.title": "Удалить резервную копию ключей",
"tooltip.restore_backup": "Восстановить резервную копию",
"btn.key_backup.create": "Создать резервную копию",
"crosssigning.tip.2": "Настроить перекрёстную подпись чтобы сохранить зашифрованные сообщения.",
"key_backup.tip": "Создайте копию Ваших зашифрованных сообщений на сервере, на случай если Вы потеряете доступ ко всем устройствам. Ваши сообщения будут защищены уникальным ключом.",
"key_backup.title": "Сохранить зашифрованные сообщения",
"settings.remove_wallpaper.title": "Удалить фон чатов",
"settings.remove_wallpaper.desc": "Вы уверены что хотите удалить фон чатов?",
"btn.remove_wallpaper": "Удалить",
"settings.theme.header": "Тема",
"settings.system_theme.title": "Использовать системную тему",
"settings.system_theme.desc": "Использовать светлую/тёмную тему согласно системным настройкам.",
"settings.twemoji.title": "Использовать эмодзи Twitter",
"settings.twemoji.desc": "Использовать эмодзи Twitter вместо системных.",
"settings.wallpaper.title": "Фон чатов",
"settings.wallpaper.desc": "Установить фон для всех чатов.",
"btn.settings.wallpaper.change": "Изменить",
"btn.settings.wallpaper.add": "Добавить",
"btn.settings.wallpaper.delete": "Удалить",
"settings.messages.header": "Сообщения",
"settings.msg_layout.title": "Вид сообщений",
"settings.msg_layout.modern": "Современный",
"settings.msg_layout.compact": "Компактный",
"settings.msg_layout.bubble": "Пузыри",
"settings.msg_spacing.title": "Отступ сообщений",
"settings.enter_newline.title": "Использовать ENTER для новой строки",
"settings.enter_newline.desc": "Использовать {0} + ENTER для отправки сообщения, а ENTER для новой строки.",
"settings.md_formatting.title": "Форматирование Markdown",
"settings.md_formatting.desc": "Форматировать сообщения с помощью синтаксиса Markdown.",
"settings.hide_membership.title": "Скрыть события участников",
"settings.hide_membership.desc": "Скрыть события участников из чата. (Вход, приглашение, выход, изгнание и бан)",
"settings.hide_profile.title": "Скрыть изменения профилей",
"settings.hide_profile.desc": "Скрыть изменения имён/аватарок из чата.",
"settings.no_media_autoload.title": "Не загружать медиафайлы",
"settings.no_media_autoload.desc": "Отключить авто-загрузку медиафайлов чтобы сохранить трафик.",
"settings.url_preview.title": "Предпросмотр ссылок",
"settings.url_preview.desc": "Показывать предпросмотр ссылок в сообщениях.",
"settings.url_preview_enc.title": "Предпросмотр ссылок в зашифрованных чатах",
"settings.url_preview_enc.desc": "Показывать предпросмотр ссылок в зашифрованных чатах.",
"settings.hidden_events.title": "Показывать скрытые события",
"settings.hidden_events.desc": "Показывать скрытые сообщения и события состояния.",
"settings.presence.header": "Присутствие",
"settings.status.title": "Статус",
"settings.status.online": "В сети",
"settings.status.offline": "Не в сети",
"settings.status.unavailable": "AFK",
"settings.ghost.title": "Режим призрака",
"settings.ghost.desc": "Не отправлять отчёты о прочтении и статус печатания.",
"settings.status_message.title": "Сообщение статуса",
"settings.status_message.text": "Введите Ваше сообщение статуса.",
"btn.status_message.set": "Сохранить",
"settings.extera.header": "Дополнительные настройки",
"settings.hide_ads.title": "Скрыть рекламу",
"settings.hide_ads.desc": "Скрыть рекламу в каналах из Telegram.",
"settings.captions.title": "Подписи к файлам (в разработке)",
"settings.captions.desc": "Отправлять подписи и файлы в одном сообщении.",
"settings.smooth_scroll.title": "Плавная прокрутка",
"settings.smooth_scroll.desc": "Использовать плавную прокрутку при получении сообщения.",
"settings.rename_tg_bot.title": "Переименовать мост Telegram в каналах.",
"settings.rename_tg_bot.desc": "Использовать имя и аватарку комнаты для бота Telegram в каналах.",
"settings.notifications.unsupported": "Уведомления не поддерживаются в этом браузере.",
"btn.notifications.request_permission": "Запросить разрешение",
"settings.notifications.header": "Уведомления и звуки",
"settings.desktop_notifications.title": "Уведомления на рабочем столе",
"settings.desktop_notifications.desc": "Показывать уведомления на рабочем столе при получении новых сообщений.",
"settings.notification_sound.title": "Звук уведомлений",
"settings.notification_sound.desc": "Проигрывать звук при получении новых сообщений.",
"settings.cross_signing.header": "Перекрёстная подпись и резервное копирование",
"settings.encryption_keys.header": "Экспорт/Импорт ключи шифрования",
"settings.export.title": "Экспорт ключи",
"settings.import.title": "Импорт ключи",
"settings.export.desc": "Экспортировать ключи для расшифровки старых сообщений на другом устройстве. Для этого Вам нужно будет указать пароль, который будет также использоваться во время импорта.",
"settings.import.desc": "Чтобы расшифровать старые сообщения, экспортируйте ключи из другого клиента. Ключи зашифрованы, поэтому Вам будет необходимо ввести пароль, чтобы их использовать.",
"tab.appearance": "Внешний вид",
"tab.presence": "Присутствие",
"tab.notifications": "Уведомления",
"tab.emoji": "Эмодзи и стикеры",
"tab.security": "Безопасность",
"tab.extera": "Дополнительные",
"tab.about": "О приложении",
"logout.title": "Выход",
"logout.confirm": "Вы уверены что хотите выйти из аккаунта?",
"btn.logout.confirm": "Выйти",
"remove_banner.title": "Удалить баннер",
"remove_banner.desc": "Вы уверены что хотите удалить баннер?",
"btn.remove_banner.confirm": "Удалить",
"settings.title": "Настройки",
"btn.remove_banner": "Удалить баннер",
"btn.logout_session": "Выйти",
"space_settings.title": "{0}{1}",
"space_settings.title.1": " — настройки пространства",
"room_aliases.publish.title": "Опубликовать в каталог комнат",
"room_settings.header": "{0}{1}",
"room_settings.header.1": " — настройки комнаты",
"command.me.desc": "Отправить действие",
"command.notice.desc": "Отправить уведомление",
"command.emote.desc": "Добавить {0} к Вашему сообщению",
"command.startdm.desc": "Начать ЛС. Пример: /startdm @amia:example.org",
"command.join.desc": "Войти в комнату по адресу. Пример: /join #extera:officialdakari.ru",
"command.invite.desc": "Пригласить пользователя. Пример: /invite @amia:example.org",
"command.disinvite.desc": "Отменить приглашение. Пример: /disinvite @amia:example.org",
"command.kick.desc": "Выгнать пользователя. Пример: /kick @amia:example.org",
"command.ban.desc": "Забанить пользователя. Пример: /ban @amia:example.org",
"command.unban.desc": "Разбанить пользователя. Пример: /unban @amia:example.org",
"command.ignore.desc": "Игнорировать пользователя. Пример: /ignore @amia:example.org",
"command.unignore.desc": "Перестать игнорировать пользователя. Пример: /unignore @amia:example.org",
"command.localnick.desc": "Изменить Ваше отображаемое имя в этой комнате.",
"command.localavatar.desc": "Изменить Вашу картинку профиля в этой комнате. Пример: /localavatar mxc://officialdakari.ru/slemrxtUERwSCLINehUdKiZk",
"command.converttodm.desc": "Пометить как ЛС",
"command.converttoroom.desc": "Пометить как комнату",
"command.premium.desc": "Как получить Premium",
"command.hide.desc": "Скрыть этот чат",
"command.unhide.desc": "Показать этот чат",
"command.hiddenlist.desc": "Показать скрытые чаты",
"create.title": " — создать {0}",
"create.title.room": "комнату",
"create.title.space": "пространство",
"tooltip.pinned": "Закреплённые сообщения",
"pinned.none": "Нет закреплённых сообщений",
"pinned.title": "Закреплённые сообщения",
"msg_menu.pin": "Закрепить",
"msg_menu.unpin": "Открепить",
"recovered.title": "Восстановленное сообщение",
"search_input.clear": "Очистить",
"settings.presence.title": "Статус"
}

70
src/push/index.js Normal file
View file

@ -0,0 +1,70 @@
import initMatrix from "../client/initMatrix";
import cons from "../client/state/cons";
export const enablePush = async () => {
const mx = initMatrix.matrixClient;
const ask = await fetch('https://extera-push.officialdakari.ru/application_server_key');
const key = await ask.json();
navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
const options = {
userVisibleOnly: true,
applicationServerKey: new Uint8Array(
key
)
};
serviceWorkerRegistration.pushManager.subscribe(options).then(
async (pushSubscription) => {
console.log(pushSubscription);
var base64 = btoa(
new Uint8Array(pushSubscription.getKey('auth'))
.reduce((data, byte) => data + String.fromCharCode(byte), '')
);
var base642 = btoa(
new Uint8Array(pushSubscription.getKey('p256dh'))
.reduce((data, byte) => data + String.fromCharCode(byte), '')
);
const k1 = await fetch(`https://extera-push.officialdakari.ru/createpushkey`, {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
endpoint: btoa(pushSubscription.endpoint),
auth: base64,
p256dh: base642
})
});
const { pushKey } = await k1.json();
const { pushers } = await mx.getPushers();
if (typeof pushKey === 'string' && !localStorage.pushkey && !pushers.find(x => x.pushkey == pushKey)) {
await mx.setPusher({
enabled: true,
app_display_name: cons.name,
app_id: cons.app_id,
data: {
url: 'https://extera-push.officialdakari.ru/_matrix/push/v1/notify',
brand: 'ExteraPush'
},
kind: 'http',
lang: 'en',
profile_tag: 'extera',
pushkey: pushKey,
device_display_name: cons.name
});
localStorage.pushkey = pushKey;
}
},
(error) => {
console.error(error);
},
);
});
}
export const disablePush = async () => {
const mx = initMatrix.matrixClient;
if (typeof localStorage.pushkey === 'string') {
await mx.removePusher(localStorage.pushkey, 'ru.officialdakari.extera');
delete localStorage.pushkey;
}
}

26
translate.js Normal file
View file

@ -0,0 +1,26 @@
const source = `src/lang/en.json`;
const dest = `src/lang/ru.json`;
import fs from 'fs';
const lang1 = JSON.parse(fs.readFileSync(source, 'utf-8'));
const lang2 = JSON.parse(fs.readFileSync(dest, 'utf-8'));
const skeys = Object.keys(lang1);
const keys = Object.keys(lang2);
const untranslatedKeys = skeys.filter(x => !keys.includes(x));
import { createInterface } from 'readline/promises';
const RL = createInterface(process.stdin, process.stdout);
(async () => {
for (const u of untranslatedKeys) {
console.log(`Source: ${lang1[u]}`);
console.log(u);
console.log(Object.keys(lang2).length, '/', Object.keys(lang1).length);
const translated = await RL.question('Target: ');
lang2[u] = translated;
fs.writeFileSync(dest, JSON.stringify(lang2, false, '\t'));
}
})();