Merge branch 'develop' into travis/feature/wellknown2
commit
14dc4b47fa
|
@ -4,7 +4,6 @@ src/component-index.js
|
|||
src/components/structures/BottomLeftMenu.js
|
||||
src/components/structures/CreateRoom.js
|
||||
src/components/structures/MessagePanel.js
|
||||
src/components/structures/NotificationPanel.js
|
||||
src/components/structures/RoomDirectory.js
|
||||
src/components/structures/RoomStatusBar.js
|
||||
src/components/structures/RoomView.js
|
||||
|
|
|
@ -65,7 +65,8 @@
|
|||
"classnames": "^2.1.2",
|
||||
"commonmark": "^0.28.1",
|
||||
"counterpart": "^0.18.0",
|
||||
"emojione": "2.2.7",
|
||||
"emojibase-data": "^4.0.0",
|
||||
"emojibase-regex": "^3.0.0",
|
||||
"file-saver": "^1.3.3",
|
||||
"filesize": "3.5.6",
|
||||
"flux": "2.1.1",
|
||||
|
|
|
@ -32,6 +32,11 @@ body {
|
|||
margin: 0px;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: $monospace-font-family;
|
||||
font-size: 100% !important;
|
||||
}
|
||||
|
||||
.error, .warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
|
@ -110,6 +115,14 @@ textarea {
|
|||
color: $primary-fg-color;
|
||||
}
|
||||
|
||||
// This is used to hide the standard outline added by browsers for
|
||||
// accessible (focusable) components. Not intended for buttons, but
|
||||
// should be used on things like focusable containers where the outline
|
||||
// is usually not helping anyone.
|
||||
.mx_HiddenFocusable {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// .mx_textinput is a container for a text input
|
||||
// + some other controls like buttons, ...
|
||||
// it has the appearance of a text box so the controls
|
||||
|
@ -445,15 +458,6 @@ textarea {
|
|||
background-color: $primary-bg-color;
|
||||
}
|
||||
|
||||
.mx_emojione {
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_emojione_selected {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background-color: $accent-color;
|
||||
color: $selection-fg-color;
|
||||
|
|
|
@ -21,6 +21,7 @@ limitations under the License.
|
|||
// padding around and in the editor.
|
||||
// Actual values from fiddling around in inspector
|
||||
margin: -7px -10px -5px -10px;
|
||||
overflow: visible !important; // override mx_EventTile_content
|
||||
|
||||
.mx_MessageEditor_editor {
|
||||
border-radius: 4px;
|
||||
|
@ -33,20 +34,28 @@ limitations under the License.
|
|||
max-height: 200px;
|
||||
overflow-x: auto;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
}
|
||||
span.mx_UserPill, span.mx_RoomPill {
|
||||
padding-left: 21px;
|
||||
position: relative;
|
||||
|
||||
span.user-pill, span.room-pill {
|
||||
border-radius: 16px;
|
||||
display: inline-block;
|
||||
color: $primary-fg-color;
|
||||
background-color: $other-user-pill-bg-color;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
// avatar psuedo element
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
content: var(--avatar-letter);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--avatar-background), $avatar-bg-color;
|
||||
color: $avatar-initial-color;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
line-height: 16px;
|
||||
font-size: 10.4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,7 +70,7 @@ limitations under the License.
|
|||
z-index: 100;
|
||||
right: 0;
|
||||
margin: 0 -110px 0 0;
|
||||
padding-right: 104px;
|
||||
padding-right: 147px;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
margin-left: 5px;
|
||||
|
@ -77,5 +86,5 @@ limitations under the License.
|
|||
|
||||
.mx_EventTile_last .mx_MessageEditor_buttons {
|
||||
position: static;
|
||||
margin-right: -103px;
|
||||
margin-right: -147px;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,12 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_EventTile_continuation {
|
||||
padding-top: 0px ! important;
|
||||
padding-top: 0px !important;
|
||||
|
||||
&.mx_EventTile_isEditing {
|
||||
padding-top: 5px !important;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_isEditing {
|
||||
|
@ -116,7 +121,7 @@ limitations under the License.
|
|||
/* HACK to override line-height which is already marked important elsewhere */
|
||||
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
|
||||
font-size: 48px ! important;
|
||||
line-height: 48px ! important;
|
||||
line-height: 52px ! important;
|
||||
}
|
||||
|
||||
/* this is used for the tile for the event which is selected via the URL.
|
||||
|
@ -157,8 +162,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_EventTile_sending .mx_UserPill,
|
||||
.mx_EventTile_sending .mx_RoomPill,
|
||||
.mx_EventTile_sending .mx_emojione {
|
||||
.mx_EventTile_sending .mx_RoomPill {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
@ -420,6 +424,7 @@ limitations under the License.
|
|||
|
||||
.mx_EventTile_content .markdown-body {
|
||||
pre, code {
|
||||
font-family: $monospace-font-family ! important;
|
||||
// deliberate constants as we're behind an invert filter
|
||||
color: #333;
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,92 +0,0 @@
|
|||
Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -15,39 +15,70 @@
|
|||
/* the 'src' links are relative to the bundle.css, which is in a subdirectory.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/*
|
||||
* Fira Mono
|
||||
* Used for monospace copy, i.e. code
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fira Mono';
|
||||
src: url('$(res)/fonts/Fira_Mono/FiraMono-Regular.ttf') format('truetype');
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Fira Mono';
|
||||
src: url('$(res)/fonts/Fira_Mono/FiraMono-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji
|
||||
* taken from https://github.com/mozilla/twemoji-colr
|
||||
* using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to
|
||||
* work on macOS
|
||||
*/
|
||||
/*
|
||||
// except we now load it dynamically via FontManager to handle browsers
|
||||
// which can't render COLR/CPAL still
|
||||
@font-face {
|
||||
font-family: "Twemoji Mozilla";
|
||||
src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2');
|
||||
}
|
||||
*/
|
|
@ -1,10 +1,13 @@
|
|||
// XXX: check this?
|
||||
/* Nunito lacks combining diacritics, so these will fall through
|
||||
to the next font. Helevetica's diacritics however do not combine
|
||||
nicely with Open Sans (on OSX, at least) and result in a huge
|
||||
horizontal mess. Arial empirically gets it right, hence prioritising
|
||||
Arial here. */
|
||||
$font-family: 'Nunito', Arial, Helvetica, Sans-Serif;
|
||||
nicely (on OSX, at least) and result in a huge horizontal mess.
|
||||
Arial empirically gets it right, hence prioritising Arial here. */
|
||||
/* We fall through to Twemoji for emoji rather than falling through
|
||||
to native Emoji fonts (if any) to ensure cross-browser consistency */
|
||||
$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Arial, Helvetica, Sans-Serif;
|
||||
|
||||
$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Courier, monospace;
|
||||
|
||||
// unified palette
|
||||
// try to use these colors when possible
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
#!/usr/bin/env node
|
||||
const EMOJI_DATA = require('emojione/emoji.json');
|
||||
const EMOJI_SUPPORTED = Object.keys(require('emojione').emojioneList);
|
||||
|
||||
// This generates src/stripped-emoji.json as used by the EmojiProvider autocomplete
|
||||
// provider.
|
||||
|
||||
const EMOJIBASE = require('emojibase-data/en/compact.json');
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const output = Object.keys(EMOJI_DATA).map(
|
||||
(key) => {
|
||||
const datum = EMOJI_DATA[key];
|
||||
const output = EMOJIBASE.map(
|
||||
(datum) => {
|
||||
const newDatum = {
|
||||
name: datum.name,
|
||||
shortname: datum.shortname,
|
||||
category: datum.category,
|
||||
emoji_order: datum.emoji_order,
|
||||
name: datum.annotation,
|
||||
shortname: `:${datum.shortcodes[0]}:`,
|
||||
category: datum.group,
|
||||
emoji_order: datum.order,
|
||||
};
|
||||
if (datum.aliases.length > 0) {
|
||||
newDatum.aliases = datum.aliases;
|
||||
if (datum.shortcodes.length > 1) {
|
||||
newDatum.aliases = datum.shortcodes.slice(1).map(s => `:${s}:`);
|
||||
}
|
||||
if (datum.aliases_ascii.length > 0) {
|
||||
newDatum.aliases_ascii = datum.aliases_ascii;
|
||||
if (datum.emoticon) {
|
||||
newDatum.aliases_ascii = [ datum.emoticon ];
|
||||
}
|
||||
return newDatum;
|
||||
}
|
||||
).filter((datum) => {
|
||||
return EMOJI_SUPPORTED.includes(datum.shortname);
|
||||
});
|
||||
);
|
||||
|
||||
// Write to a file in src. Changes should be checked into git. This file is copied by
|
||||
// babel using --copy-files
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
import {ContentRepo} from 'matrix-js-sdk';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
|
||||
module.exports = {
|
||||
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
||||
|
@ -58,4 +59,71 @@ module.exports = {
|
|||
}
|
||||
return require('../res/img/' + images[total % images.length] + '.png');
|
||||
},
|
||||
|
||||
/**
|
||||
* returns the first (non-sigil) character of 'name',
|
||||
* converted to uppercase
|
||||
* @param {string} name
|
||||
* @return {string} the first letter
|
||||
*/
|
||||
getInitialLetter(name) {
|
||||
if (name.length < 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
const initial = name[0];
|
||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
// string.codePointAt(0) would do this, but that isn't supported by
|
||||
// some browsers (notably PhantomJS).
|
||||
let chars = 1;
|
||||
const first = name.charCodeAt(idx);
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
||||
const second = name.charCodeAt(idx+1);
|
||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||||
chars++;
|
||||
}
|
||||
}
|
||||
|
||||
const firstChar = name.substring(idx, idx+chars);
|
||||
return firstChar.toUpperCase();
|
||||
},
|
||||
|
||||
avatarUrlForRoom(room, width, height, resizeMethod) {
|
||||
const explicitRoomAvatar = room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
false,
|
||||
);
|
||||
if (explicitRoomAvatar) {
|
||||
return explicitRoomAvatar;
|
||||
}
|
||||
|
||||
let otherMember = null;
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (otherUserId) {
|
||||
otherMember = room.getMember(otherUserId);
|
||||
} else {
|
||||
// if the room is not marked as a 1:1, but only has max 2 members
|
||||
// then still try to show any avatar (pref. other member)
|
||||
otherMember = room.getAvatarFallbackMember();
|
||||
}
|
||||
if (otherMember) {
|
||||
return otherMember.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
|
122
src/HtmlUtils.js
122
src/HtmlUtils.js
|
@ -27,22 +27,18 @@ import linkifyMatrix from './linkify-matrix';
|
|||
import _linkifyElement from 'linkifyjs/element';
|
||||
import _linkifyString from 'linkifyjs/string';
|
||||
import escape from 'lodash/escape';
|
||||
import emojione from 'emojione';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import url from 'url';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||
|
||||
emojione.imagePathSVG = 'emojione/svg/';
|
||||
// Store PNG path for displaying many flags at once (for increased performance over SVG)
|
||||
emojione.imagePathPNG = 'emojione/png/';
|
||||
// Use SVGs for emojis
|
||||
emojione.imageType = 'svg';
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
// Anything outside the basic multilingual plane will be a surrogate pair
|
||||
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
||||
// And there a bunch more symbol characters that emojione has within the
|
||||
// And there a bunch more symbol characters that emojibase has within the
|
||||
// BMP, so this includes the ranges from 'letterlike symbols' to
|
||||
// 'miscellaneous symbols and arrows' which should catch all of them
|
||||
// (with plenty of false positives, but that's OK)
|
||||
|
@ -54,15 +50,15 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
|
|||
// Regex pattern for whitespace characters
|
||||
const WHITESPACE_REGEX = new RegExp("\\s", "g");
|
||||
|
||||
// And this is emojione's complete regex
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
|
||||
|
||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||
|
||||
/*
|
||||
* Return true if the given string contains emoji
|
||||
* Uses a much, much simpler regex than emojione's so will give false
|
||||
* Uses a much, much simpler regex than emojibase's so will give false
|
||||
* positives, but useful for fast-path testing strings to see if they
|
||||
* need emojification.
|
||||
* unicodeToImage uses this function.
|
||||
|
@ -71,73 +67,27 @@ export function containsEmoji(str) {
|
|||
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||
}
|
||||
|
||||
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
||||
* because we want to include emoji shortnames in title text
|
||||
*/
|
||||
function unicodeToImage(str, addAlt) {
|
||||
if (addAlt === undefined) addAlt = true;
|
||||
|
||||
let replaceWith; let unicode; let short; let fname;
|
||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||
|
||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
|
||||
// if the unicodeChar doesnt exist just return the entire match
|
||||
return unicodeChar;
|
||||
} else {
|
||||
// get the unicode codepoint from the actual char
|
||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
||||
|
||||
short = mappedUnicode[unicode];
|
||||
fname = emojione.emojioneList[short].fname;
|
||||
|
||||
// depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname
|
||||
const title = mappedUnicode[unicode];
|
||||
|
||||
if (addAlt) {
|
||||
const alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
|
||||
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
|
||||
} else {
|
||||
replaceWith = `<img class="mx_emojione" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
|
||||
}
|
||||
return replaceWith;
|
||||
}
|
||||
});
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shortcode for an emoji character.
|
||||
*
|
||||
* @param {String} char The emoji character
|
||||
* @return {String} The shortcode (such as :thumbup:)
|
||||
*/
|
||||
export function unicodeToShort(char) {
|
||||
const unicode = emojione.jsEscapeMap[char];
|
||||
return emojione.mapUnicodeToShort()[unicode];
|
||||
export function unicodeToShortcode(char) {
|
||||
const data = EMOJIBASE.find(e => e.unicode === char);
|
||||
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Given one or more unicode characters (represented by unicode
|
||||
* character number), return an image node with the corresponding
|
||||
* emoji.
|
||||
* Returns the unicode character for an emoji shortcode
|
||||
*
|
||||
* @param alt {string} String to use for the image alt text
|
||||
* @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used.
|
||||
* @param unicode {integer} One or more integers representing unicode characters
|
||||
* @returns A img node with the corresponding emoji
|
||||
* @param {String} shortcode The shortcode (such as :thumbup:)
|
||||
* @return {String} The emoji character; null if none exists
|
||||
*/
|
||||
export function charactersToImageNode(alt, useSvg, ...unicode) {
|
||||
const fileName = unicode.map((u) => {
|
||||
return u.toString(16);
|
||||
}).join('-');
|
||||
const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
|
||||
const fileType = useSvg ? 'svg' : 'png';
|
||||
return <img
|
||||
alt={alt}
|
||||
src={`${path}${fileName}.${fileType}${emojione.cacheBustParam}`}
|
||||
/>;
|
||||
export function shortcodeToUnicode(shortcode) {
|
||||
shortcode = shortcode.slice(1, shortcode.length - 1);
|
||||
const data = EMOJIBASE.find(e => e.shortcodes && e.shortcodes.includes(shortcode));
|
||||
return data ? data.unicode : null;
|
||||
}
|
||||
|
||||
export function processHtmlForSending(html: string): string {
|
||||
|
@ -444,13 +394,10 @@ class TextHighlighter extends BaseHighlighter {
|
|||
* opts.disableBigEmoji: optional argument to disable the big emoji class.
|
||||
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
|
||||
* opts.returnString: return an HTML string rather than JSX elements
|
||||
* opts.emojiOne: optional param to do emojiOne (default true)
|
||||
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
||||
*/
|
||||
export function bodyToHtml(content, highlights, opts={}) {
|
||||
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
||||
|
||||
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
|
||||
let bodyHasEmoji = false;
|
||||
|
||||
let sanitizeParams = sanitizeHtmlParams;
|
||||
|
@ -481,28 +428,12 @@ export function bodyToHtml(content, highlights, opts={}) {
|
|||
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
|
||||
strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
|
||||
|
||||
if (doEmojiOne) {
|
||||
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
|
||||
}
|
||||
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
|
||||
|
||||
// Only generate safeBody if the message was sent as org.matrix.custom.html
|
||||
if (isHtmlMessage) {
|
||||
isDisplayedWithHtml = true;
|
||||
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
|
||||
} else {
|
||||
// ... or if there are emoji, which we insert as HTML alongside the
|
||||
// escaped plaintext body.
|
||||
if (bodyHasEmoji) {
|
||||
isDisplayedWithHtml = true;
|
||||
safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams);
|
||||
}
|
||||
}
|
||||
|
||||
// An HTML message with emoji
|
||||
// or a plaintext message with emoji that was escaped and sanitized into
|
||||
// HTML.
|
||||
if (bodyHasEmoji) {
|
||||
safeBody = unicodeToImage(safeBody);
|
||||
}
|
||||
} finally {
|
||||
delete sanitizeParams.textFilter;
|
||||
|
@ -514,7 +445,6 @@ export function bodyToHtml(content, highlights, opts={}) {
|
|||
|
||||
let emojiBody = false;
|
||||
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
|
||||
|
||||
// Ignore spaces in body text. Emojis with spaces in between should
|
||||
|
@ -526,12 +456,14 @@ export function bodyToHtml(content, highlights, opts={}) {
|
|||
// presented as large.
|
||||
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, '');
|
||||
|
||||
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length
|
||||
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
|
||||
// Prevent user pills expanding for users with only emoji in
|
||||
// their username
|
||||
&& (content.formatted_body == undefined
|
||||
|| !content.formatted_body.includes("https://matrix.to/"));
|
||||
(
|
||||
content.formatted_body == undefined ||
|
||||
!content.formatted_body.includes("https://matrix.to/")
|
||||
);
|
||||
}
|
||||
|
||||
const className = classNames({
|
||||
|
@ -545,12 +477,6 @@ export function bodyToHtml(content, highlights, opts={}) {
|
|||
<span key="body" className={className} dir="auto">{ strippedBody }</span>;
|
||||
}
|
||||
|
||||
export function emojifyText(text, addAlt) {
|
||||
return {
|
||||
__html: unicodeToImage(escape(text), addAlt),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies the given string. This is a wrapper around 'linkifyjs/string'.
|
||||
*
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
Copyright 2015 - 2017 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as emojione from 'emojione';
|
||||
|
||||
|
||||
export function unicodeToEmojiUri(str) {
|
||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||
|
||||
// remove any zero width joiners/spaces used in conjugate emojis as the emojione URIs don't contain them
|
||||
return str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||
if ((typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap))) {
|
||||
// if the unicodeChar doesn't exist just return the entire match
|
||||
return unicodeChar;
|
||||
} else {
|
||||
// get the unicode codepoint from the actual char
|
||||
const unicode = emojione.jsEscapeMap[unicodeChar];
|
||||
|
||||
const short = mappedUnicode[unicode];
|
||||
const fname = emojione.emojioneList[short].fname;
|
||||
|
||||
return emojione.imagePathSVG+fname+'.svg'+emojione.cacheBustParam;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -19,47 +19,31 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import sdk from '../index';
|
||||
import {PillCompletion} from './Components';
|
||||
import type {Completion, SelectionRange} from './Autocompleter';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
import EmojiData from '../stripped-emoji.json';
|
||||
|
||||
const LIMIT = 20;
|
||||
const CATEGORY_ORDER = [
|
||||
'people',
|
||||
'food',
|
||||
'objects',
|
||||
'activity',
|
||||
'nature',
|
||||
'travel',
|
||||
'flags',
|
||||
'regional',
|
||||
'symbols',
|
||||
'modifier',
|
||||
];
|
||||
|
||||
// Match for ":wink:" or ascii-style ";-)" provided by emojione
|
||||
// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a
|
||||
// whitespace character or an emoji before the emoji. The reason for unicodeRegexp is
|
||||
// that we need to support inputting multiple emoji with no space between them.
|
||||
const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:[+-\\w]*:?)$', 'g');
|
||||
|
||||
// We also need to match the non-zero-length prefixes to remove them from the final match,
|
||||
// and update the range so that we don't replace the whitespace or the previous emoji.
|
||||
const MATCH_PREFIX_REGEX = new RegExp('(\\s|' + unicodeRegexp + ')');
|
||||
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase
|
||||
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g');
|
||||
|
||||
// XXX: it's very unclear why we bother with this generated emojidata file.
|
||||
// all it means is that we end up bloating the bundle with precomputed stuff
|
||||
// which would be trivial to calculate and cache on demand.
|
||||
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
|
||||
(a, b) => {
|
||||
if (a.category === b.category) {
|
||||
return a.emoji_order - b.emoji_order;
|
||||
}
|
||||
return CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
|
||||
return a.category - b.category;
|
||||
},
|
||||
).map((a, index) => {
|
||||
return {
|
||||
|
@ -101,26 +85,20 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
return []; // don't give any suggestions if the user doesn't want them
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let completions = [];
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
let matchedString = command[0];
|
||||
|
||||
// Remove prefix of any length (single whitespace or unicode emoji)
|
||||
const prefixMatch = MATCH_PREFIX_REGEX.exec(matchedString);
|
||||
if (prefixMatch) {
|
||||
matchedString = matchedString.slice(prefixMatch[0].length);
|
||||
range.start += prefixMatch[0].length;
|
||||
}
|
||||
const matchedString = command[0];
|
||||
completions = this.matcher.match(matchedString);
|
||||
|
||||
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
||||
completions = completions.concat(this.nameMatcher.match(matchedString));
|
||||
|
||||
const sorters = [];
|
||||
// First, sort by score (Infinity if matchedString not in shortname)
|
||||
// make sure that emoticons come first
|
||||
sorters.push((c) => score(matchedString, c.aliases_ascii));
|
||||
|
||||
// then sort by score (Infinity if matchedString not in shortname)
|
||||
sorters.push((c) => score(matchedString, c.shortname));
|
||||
// If the matchedString is not empty, sort by length of shortname. Example:
|
||||
// matchedString = ":bookmark"
|
||||
|
@ -133,12 +111,12 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
completions = _sortBy(_uniq(completions), sorters);
|
||||
|
||||
completions = completions.map((result) => {
|
||||
const {shortname} = result;
|
||||
const unicode = shortnameToUnicode(shortname);
|
||||
const { shortname } = result;
|
||||
const unicode = shortcodeToUnicode(shortname);
|
||||
return {
|
||||
completion: unicode,
|
||||
component: (
|
||||
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} />
|
||||
<PillCompletion title={shortname} initialComponent={<span style={{maxWidth: '1em'}}>{ unicode }</span>} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
|
|
|
@ -129,7 +129,7 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
|
||||
const yRetention = 1.0;
|
||||
|
||||
if (Math.abs(e.deltaX) < xyThreshold) {
|
||||
if (Math.abs(e.deltaX) <= xyThreshold) {
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
this._scrollElement.scrollLeft += e.deltaY * yRetention;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -60,7 +61,7 @@ export default React.createClass({
|
|||
inputs: PropTypes.object,
|
||||
|
||||
// As js-sdk interactive-auth
|
||||
makeRegistrationUrl: PropTypes.func,
|
||||
requestEmailToken: PropTypes.func,
|
||||
sessionId: PropTypes.string,
|
||||
clientSecret: PropTypes.string,
|
||||
emailSid: PropTypes.string,
|
||||
|
@ -96,6 +97,7 @@ export default React.createClass({
|
|||
sessionId: this.props.sessionId,
|
||||
clientSecret: this.props.clientSecret,
|
||||
emailSid: this.props.emailSid,
|
||||
requestEmailToken: this.props.requestEmailToken,
|
||||
});
|
||||
|
||||
this._authLogic.attemptAuth().then((result) => {
|
||||
|
@ -202,7 +204,6 @@ export default React.createClass({
|
|||
stageState={this.state.stageState}
|
||||
fail={this._onAuthStageFailed}
|
||||
setEmailSid={this._setEmailSid}
|
||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
||||
showContinue={!this.props.continueIsManaged}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -24,6 +24,7 @@ import { DragDropContext } from 'react-beautiful-dnd';
|
|||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
|
@ -118,6 +119,8 @@ const LoggedInView = React.createClass({
|
|||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
fixupColorFonts();
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -322,6 +325,18 @@ const LoggedInView = React.createClass({
|
|||
handled = true;
|
||||
}
|
||||
break;
|
||||
case KeyCode.KEY_I:
|
||||
// Ideally this would be CTRL+P for "Profile", but that's
|
||||
// taken by the print dialog. CTRL+I for "Information"
|
||||
// will have to do.
|
||||
|
||||
if (ctrlCmdOnly) {
|
||||
dis.dispatch({
|
||||
action: 'toggle_top_left_menu',
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
|
|
|
@ -96,6 +96,9 @@ module.exports = React.createClass({
|
|||
|
||||
// helper function to access relations for an event
|
||||
getRelationsForEvent: PropTypes.func,
|
||||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -541,6 +544,7 @@ module.exports = React.createClass({
|
|||
last={last}
|
||||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
/>
|
||||
</li>,
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,12 +16,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import { _t } from '../../languageHandler';
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
const sdk = require('../../index');
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const dis = require("../../dispatcher");
|
||||
|
||||
/*
|
||||
* Component which shows the global notification list using a TimelinePanel
|
||||
|
@ -44,7 +42,7 @@ const NotificationPanel = React.createClass({
|
|||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview = {false}
|
||||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
empty={_t('You have no visible notifications')}
|
||||
/>
|
||||
|
|
|
@ -304,8 +304,6 @@ module.exports = React.createClass({
|
|||
|
||||
// return suitable content for the main (text) part of the status bar.
|
||||
_getContent: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
|
|
|
@ -29,6 +29,7 @@ import { Group } from 'matrix-js-sdk';
|
|||
import PropTypes from 'prop-types';
|
||||
import RoomTile from "../views/rooms/RoomTile";
|
||||
import LazyRenderList from "../views/elements/LazyRenderList";
|
||||
import {_t} from "../../languageHandler";
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
const debug = false;
|
||||
|
@ -42,6 +43,7 @@ const RoomSubList = React.createClass({
|
|||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
addRoomLabel: PropTypes.string,
|
||||
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
|
@ -232,7 +234,11 @@ const RoomSubList = React.createClass({
|
|||
let addRoomButton;
|
||||
if (this.props.onAddRoom) {
|
||||
addRoomButton = (
|
||||
<AccessibleButton onClick={ this.props.onAddRoom } className="mx_RoomSubList_addRoom" />
|
||||
<AccessibleButton
|
||||
onClick={ this.props.onAddRoom }
|
||||
className="mx_RoomSubList_addRoom"
|
||||
title={this.props.addRoomLabel || _t("Add room")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1832,6 +1832,7 @@ module.exports = React.createClass({
|
|||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
|
|
@ -106,6 +106,9 @@ const TimelinePanel = React.createClass({
|
|||
|
||||
// placeholder text to use if the timeline is empty
|
||||
empty: PropTypes.string,
|
||||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -553,6 +556,9 @@ const TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
onEventDecrypted: function(ev) {
|
||||
// Can be null for the notification timeline, etc.
|
||||
if (!this.props.timelineSet.room) return;
|
||||
|
||||
// Need to update as we don't display event tiles for events that
|
||||
// haven't yet been decrypted. The event will have just been updated
|
||||
// in place so we just need to re-render.
|
||||
|
@ -1261,6 +1267,7 @@ const TimelinePanel = React.createClass({
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
editEvent={this.state.editEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -23,6 +24,8 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
|
|||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import Avatar from '../../Avatar';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from "../../dispatcher";
|
||||
import {focusCapturedRef} from "../../utils/Accessibility";
|
||||
|
||||
const AVATAR_SIZE = 28;
|
||||
|
||||
|
@ -37,6 +40,7 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
super();
|
||||
this.state = {
|
||||
menuDisplayed: false,
|
||||
menuFunctions: null, // should be { close: fn }
|
||||
profileInfo: null,
|
||||
};
|
||||
|
||||
|
@ -59,6 +63,8 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
try {
|
||||
const profileInfo = await this._getProfileInfo();
|
||||
this.setState({profileInfo});
|
||||
|
@ -68,6 +74,17 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
// For accessibility
|
||||
if (payload.action === "toggle_top_left_menu") {
|
||||
if (this._buttonRef) this._buttonRef.click();
|
||||
}
|
||||
};
|
||||
|
||||
_getDisplayName() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return _t("Guest");
|
||||
|
@ -88,7 +105,13 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_TopLeftMenuButton" onClick={this.onToggleMenu}>
|
||||
<AccessibleButton
|
||||
className="mx_TopLeftMenuButton"
|
||||
role="button"
|
||||
onClick={this.onToggleMenu}
|
||||
inputRef={(r) => this._buttonRef = r}
|
||||
aria-label={_t("Your profile")}
|
||||
>
|
||||
<BaseAvatar
|
||||
idName={MatrixClientPeg.get().getUserId()}
|
||||
name={name}
|
||||
|
@ -98,7 +121,7 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
resizeMethod="crop"
|
||||
/>
|
||||
{ nameElement }
|
||||
<span className="mx_TopLeftMenuButton_chevron"></span>
|
||||
<span className="mx_TopLeftMenuButton_chevron" />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -107,20 +130,26 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.state.menuDisplayed && this.state.menuFunctions) {
|
||||
this.state.menuFunctions.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const elementRect = e.currentTarget.getBoundingClientRect();
|
||||
const x = elementRect.left;
|
||||
const y = elementRect.top + elementRect.height;
|
||||
|
||||
ContextualMenu.createMenu(TopLeftMenu, {
|
||||
const menuFunctions = ContextualMenu.createMenu(TopLeftMenu, {
|
||||
chevronFace: "none",
|
||||
left: x,
|
||||
top: y,
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
displayName: this._getDisplayName(),
|
||||
containerRef: focusCapturedRef, // Focus the TopLeftMenu on first render
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
this.setState({ menuDisplayed: false, menuFunctions: null });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
this.setState({ menuDisplayed: true, menuFunctions });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -187,6 +188,20 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_requestEmailToken: function(emailAddress, clientSecret, sendAttempt, sessionId) {
|
||||
return this._matrixClient.requestRegisterEmailToken(
|
||||
emailAddress,
|
||||
clientSecret,
|
||||
sendAttempt,
|
||||
this.props.makeRegistrationUrl({
|
||||
client_secret: clientSecret,
|
||||
hs_url: this._matrixClient.getHomeserverUrl(),
|
||||
is_url: this._matrixClient.getIdentityServerUrl(),
|
||||
session_id: sessionId,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
_onUIAuthFinished: async function(success, response, extra) {
|
||||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
|
@ -400,7 +415,7 @@ module.exports = React.createClass({
|
|||
makeRequest={this._makeRegisterRequest}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={this._getUIAuthInputs()}
|
||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
||||
requestEmailToken={this._requestEmailToken}
|
||||
sessionId={this.props.sessionId}
|
||||
clientSecret={this.props.clientSecret}
|
||||
emailSid={this.props.idSid}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -57,7 +58,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
* session to be failed and the process to go back to the start.
|
||||
* setEmailSid: m.login.email.identity only: a function to be called with the
|
||||
* email sid after a token is requested.
|
||||
* makeRegistrationUrl A function that makes a registration URL
|
||||
*
|
||||
* Each component may also provide the following functions (beyond the standard React ones):
|
||||
* focus: set the input focus appropriately in the form.
|
||||
|
@ -365,7 +365,6 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
stageState: PropTypes.object.isRequired,
|
||||
fail: PropTypes.func.isRequired,
|
||||
setEmailSid: PropTypes.func.isRequired,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -374,38 +373,6 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
if (this.props.stageState.emailSid === null) {
|
||||
this.setState({requestingToken: true});
|
||||
this._requestEmailToken().catch((e) => {
|
||||
this.props.fail(e);
|
||||
}).finally(() => {
|
||||
this.setState({requestingToken: false});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Requests a verification token by email.
|
||||
*/
|
||||
_requestEmailToken: function() {
|
||||
const nextLink = this.props.makeRegistrationUrl({
|
||||
client_secret: this.props.clientSecret,
|
||||
hs_url: this.props.matrixClient.getHomeserverUrl(),
|
||||
is_url: this.props.matrixClient.getIdentityServerUrl(),
|
||||
session_id: this.props.authSessionId,
|
||||
});
|
||||
|
||||
return this.props.matrixClient.requestRegisterEmailToken(
|
||||
this.props.inputs.emailAddress,
|
||||
this.props.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
nextLink,
|
||||
).then((result) => {
|
||||
this.props.setEmailSid(result.sid);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.requestingToken) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
|
|
@ -133,40 +133,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* returns the first (non-sigil) character of 'name',
|
||||
* converted to uppercase
|
||||
*/
|
||||
_getInitialLetter: function(name) {
|
||||
if (name.length < 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
const initial = name[0];
|
||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
// string.codePointAt(0) would do this, but that isn't supported by
|
||||
// some browsers (notably PhantomJS).
|
||||
let chars = 1;
|
||||
const first = name.charCodeAt(idx);
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
||||
const second = name.charCodeAt(idx+1);
|
||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||||
chars++;
|
||||
}
|
||||
}
|
||||
|
||||
const firstChar = name.substring(idx, idx+chars);
|
||||
return firstChar.toUpperCase();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
const {
|
||||
|
@ -176,20 +143,20 @@ module.exports = React.createClass({
|
|||
} = this.props;
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
const initialLetter = this._getInitialLetter(name);
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (width * 0.65) + "px",
|
||||
width: width + "px",
|
||||
lineHeight: height + "px" }}
|
||||
>
|
||||
{ initialLetter }
|
||||
</EmojiText>
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={title} onError={this.onError}
|
||||
width={width} height={height} />
|
||||
width={width} height={height} aria-hidden="true" />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
|
|
|
@ -19,7 +19,7 @@ import {ContentRepo} from "matrix-js-sdk";
|
|||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from "../../../index";
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import Avatar from '../../../Avatar';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
@ -89,7 +89,6 @@ module.exports = React.createClass({
|
|||
props.resizeMethod,
|
||||
), // highest priority
|
||||
this.getRoomAvatarUrl(props),
|
||||
this.getOneToOneAvatar(props), // lowest priority
|
||||
].filter(function(url) {
|
||||
return (url != null && url != "");
|
||||
});
|
||||
|
@ -98,41 +97,14 @@ module.exports = React.createClass({
|
|||
getRoomAvatarUrl: function(props) {
|
||||
if (!props.room) return null;
|
||||
|
||||
return props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
return Avatar.avatarUrlForRoom(
|
||||
props.room,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
},
|
||||
|
||||
getOneToOneAvatar: function(props) {
|
||||
const room = props.room;
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
let otherMember = null;
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (otherUserId) {
|
||||
otherMember = room.getMember(otherUserId);
|
||||
} else {
|
||||
// if the room is not marked as a 1:1, but only has max 2 members
|
||||
// then still try to show any avatar (pref. other member)
|
||||
otherMember = room.getAvatarFallbackMember();
|
||||
}
|
||||
if (otherMember) {
|
||||
return otherMember.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
onRoomAvatarClick: function() {
|
||||
const avatarUrl = this.props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -29,6 +30,10 @@ export class TopLeftMenu extends React.Component {
|
|||
displayName: PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func,
|
||||
|
||||
// Optional function to collect a reference to the container
|
||||
// of this component directly.
|
||||
containerRef: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -61,44 +66,48 @@ export class TopLeftMenu extends React.Component {
|
|||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex="0">{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener">
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener" aria-hidden={true}>
|
||||
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let homePageSection = null;
|
||||
let homePageItem = null;
|
||||
if (this.hasHomePage()) {
|
||||
homePageSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
|
||||
</ul>;
|
||||
homePageItem = <li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage} tabIndex={0}>
|
||||
{_t("Home")}
|
||||
</li>;
|
||||
}
|
||||
|
||||
let signInOutSection;
|
||||
let signInOutItem;
|
||||
if (isGuest) {
|
||||
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>{_t("Sign in")}</li>
|
||||
</ul>;
|
||||
signInOutItem = <li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn} tabIndex={0}>
|
||||
{_t("Sign in")}
|
||||
</li>;
|
||||
} else {
|
||||
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li>
|
||||
</ul>;
|
||||
signInOutItem = <li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut} tabIndex={0}>
|
||||
{_t("Sign out")}
|
||||
</li>;
|
||||
}
|
||||
|
||||
return <div className="mx_TopLeftMenu">
|
||||
<div className="mx_TopLeftMenu_section_noIcon">
|
||||
const settingsItem = <li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings} tabIndex={0}>
|
||||
{_t("Settings")}
|
||||
</li>;
|
||||
|
||||
return <div className="mx_TopLeftMenu mx_HiddenFocusable" tabIndex={0} ref={this.props.containerRef}>
|
||||
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}>
|
||||
<div>{this.props.displayName}</div>
|
||||
<div className="mx_TopLeftMenu_greyedText">{this.props.userId}</div>
|
||||
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
|
||||
{hostingSignup}
|
||||
</div>
|
||||
{homePageSection}
|
||||
<ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
|
||||
{homePageItem}
|
||||
{settingsItem}
|
||||
{signInOutItem}
|
||||
</ul>
|
||||
{signInOutSection}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -63,8 +63,12 @@ export default function AccessibleButton(props) {
|
|||
};
|
||||
}
|
||||
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = "button";
|
||||
// Pass through the ref - used for keyboard shortcut access to some buttons
|
||||
restProps.ref = restProps.inputRef;
|
||||
delete restProps.inputRef;
|
||||
|
||||
restProps.tabIndex = restProps.tabIndex === undefined ? "0" : restProps.tabIndex;
|
||||
restProps.role = restProps.role === undefined ? "button" : restProps.role;
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||
"mx_AccessibleButton";
|
||||
|
||||
|
@ -89,6 +93,7 @@ export default function AccessibleButton(props) {
|
|||
*/
|
||||
AccessibleButton.propTypes = {
|
||||
children: PropTypes.node,
|
||||
inputRef: PropTypes.func,
|
||||
element: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||
|
||||
export default function EmojiText(props) {
|
||||
const {element, children, addAlt, ...restProps} = props;
|
||||
|
||||
// fast path: simple regex to detect strings that don't contain
|
||||
// emoji and just return them
|
||||
if (containsEmoji(children)) {
|
||||
restProps.dangerouslySetInnerHTML = emojifyText(children, addAlt);
|
||||
return React.createElement(element, restProps);
|
||||
} else {
|
||||
return React.createElement(element, restProps, children);
|
||||
}
|
||||
}
|
||||
|
||||
EmojiText.propTypes = {
|
||||
element: PropTypes.string,
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
EmojiText.defaultProps = {
|
||||
element: 'span',
|
||||
addAlt: true,
|
||||
};
|
|
@ -45,12 +45,18 @@ class FlairAvatar extends React.Component {
|
|||
const tooltip = this.props.groupProfile.name ?
|
||||
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
||||
this.props.groupProfile.groupId;
|
||||
|
||||
// Note: we hide flair from screen readers but ideally we'd support
|
||||
// reading something out on hover. There's no easy way to do this though,
|
||||
// so instead we just hide it completely.
|
||||
return <img
|
||||
src={httpUrl}
|
||||
width="16"
|
||||
height="16"
|
||||
onClick={this.onClick}
|
||||
title={tooltip} />;
|
||||
title={tooltip}
|
||||
aria-hidden={true}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -117,13 +117,9 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
return (
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
<EmojiText>
|
||||
{ summaries.join(", ") }
|
||||
</EmojiText>
|
||||
{ summaries.join(", ") }
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -27,6 +28,7 @@ import Autocomplete from '../rooms/Autocomplete';
|
|||
import {PartCreator} from '../../../editor/parts';
|
||||
import {renderModel} from '../../../editor/render';
|
||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class MessageEditor extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -40,16 +42,17 @@ export default class MessageEditor extends React.Component {
|
|||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
const room = this.context.matrixClient.getRoom(this.props.event.getRoomId());
|
||||
const partCreator = new PartCreator(
|
||||
() => this._autocompleteRef,
|
||||
query => this.setState({query}),
|
||||
room,
|
||||
);
|
||||
this.model = new EditorModel(
|
||||
parseEvent(this.props.event),
|
||||
parseEvent(this.props.event, room),
|
||||
partCreator,
|
||||
this._updateEditorState,
|
||||
);
|
||||
const room = this.context.matrixClient.getRoom(this.props.event.getRoomId());
|
||||
this.state = {
|
||||
autoComplete: null,
|
||||
room,
|
||||
|
@ -176,7 +179,7 @@ export default class MessageEditor extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <div className="mx_MessageEditor">
|
||||
return <div className={classNames("mx_MessageEditor", this.props.className)}>
|
||||
{ autoComplete }
|
||||
<div
|
||||
className="mx_MessageEditor_editor"
|
||||
|
@ -185,6 +188,7 @@ export default class MessageEditor extends React.Component {
|
|||
onInput={this._onInput}
|
||||
onKeyDown={this._onKeyDown}
|
||||
ref={ref => this._editorRef = ref}
|
||||
aria-label={_t("Edit message")}
|
||||
></div>
|
||||
<div className="mx_MessageEditor_buttons">
|
||||
<AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
|
||||
|
|
|
@ -117,7 +117,6 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
const groupName = this.props.group.name || this.props.group.groupId;
|
||||
const httpAvatarUrl = this.props.group.avatarUrl ?
|
||||
|
@ -129,9 +128,9 @@ export default React.createClass({
|
|||
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
const label = <EmojiText element="div" title={this.props.group.groupId} className={nameClasses} dir="auto">
|
||||
const label = <div title={this.props.group.groupId} className={nameClasses} dir="auto">
|
||||
{ groupName }
|
||||
</EmojiText>;
|
||||
</div>;
|
||||
|
||||
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
||||
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
||||
|
|
|
@ -180,7 +180,6 @@ module.exports = React.createClass({
|
|||
this.props.groupMember.displayname || this.props.groupMember.userId
|
||||
);
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
|
@ -189,7 +188,7 @@ module.exports = React.createClass({
|
|||
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
{ avatarElement }
|
||||
<EmojiText element="h2">{ groupMemberName }</EmojiText>
|
||||
<h2>{ groupMemberName }</h2>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
|
|
|
@ -149,7 +149,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
@ -221,7 +220,7 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>
|
||||
{ avatarElement }
|
||||
|
||||
<EmojiText element="h2">{ groupRoomName }</EmojiText>
|
||||
<h2>{ groupRoomName }</h2>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
|
|
|
@ -23,12 +23,17 @@ export default class MessageTimestamp extends React.Component {
|
|||
static propTypes = {
|
||||
ts: PropTypes.number.isRequired,
|
||||
showTwelveHour: PropTypes.bool,
|
||||
ariaHidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const date = new Date(this.props.ts);
|
||||
return (
|
||||
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)}>
|
||||
<span
|
||||
className="mx_MessageTimestamp"
|
||||
title={formatFullDate(date, this.props.showTwelveHour)}
|
||||
aria-hidden={this.props.ariaHidden}
|
||||
>
|
||||
{ formatTime(date, this.props.showTwelveHour) }
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import { unicodeToShort } from '../../../HtmlUtils';
|
||||
import { unicodeToShortcode } from '../../../HtmlUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
|
||||
|
@ -46,7 +46,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
|
|||
const { name } = room.getMember(reactionEvent.getSender());
|
||||
senders.push(name);
|
||||
}
|
||||
const shortName = unicodeToShort(content) || content;
|
||||
const shortName = unicodeToShortcode(content);
|
||||
tooltipLabel = <div>{_t(
|
||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
|
||||
{
|
||||
|
@ -59,6 +59,9 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
|
|||
</div>;
|
||||
},
|
||||
reactedWith: (sub) => {
|
||||
if (!shortName) {
|
||||
return null;
|
||||
}
|
||||
return <div className="mx_ReactionsRowButtonTooltip_reactedWith">
|
||||
{sub}
|
||||
</div>;
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -95,7 +94,6 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const {mxEvent} = this.props;
|
||||
const colorClass = getUserNameColorClass(mxEvent.getSender());
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
|
@ -117,7 +115,7 @@ export default React.createClass({
|
|||
/>;
|
||||
}
|
||||
|
||||
const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;
|
||||
const nameElem = name || '';
|
||||
|
||||
// Name + flair
|
||||
const nameFlair = <span>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -136,7 +137,6 @@ module.exports = React.createClass({
|
|||
if (messageWasEdited || stoppedEditing) {
|
||||
this._applyFormatting();
|
||||
}
|
||||
this.calculateUrlPreview();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -162,7 +162,7 @@ module.exports = React.createClass({
|
|||
calculateUrlPreview: function() {
|
||||
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
if (this.props.showUrlPreview && !this.state.links.length) {
|
||||
if (this.props.showUrlPreview) {
|
||||
let links = this.findLinks(this.refs.content.children);
|
||||
if (links.length) {
|
||||
// de-dup the links (but preserve ordering)
|
||||
|
@ -471,9 +471,8 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
if (this.props.isEditing) {
|
||||
const MessageEditor = sdk.getComponent('elements.MessageEditor');
|
||||
return <MessageEditor event={this.props.mxEvent} />;
|
||||
return <MessageEditor event={this.props.mxEvent} className="mx_EventTile_content" />;
|
||||
}
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
||||
|
@ -512,12 +511,12 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
||||
*
|
||||
<EmojiText
|
||||
<span
|
||||
className="mx_MEmoteBody_sender"
|
||||
onClick={this.onEmoteSenderClick}
|
||||
>
|
||||
{ name }
|
||||
</EmojiText>
|
||||
</span>
|
||||
|
||||
{ body }
|
||||
{ widgets }
|
||||
|
|
|
@ -20,7 +20,6 @@ const React = require('react');
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
const TextForEvent = require('../../../TextForEvent');
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TextualEvent',
|
||||
|
@ -31,11 +30,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||
if (text == null || text.length === 0) return null;
|
||||
return (
|
||||
<EmojiText element="div" className="mx_TextualEvent">{ text }</EmojiText>
|
||||
<div className="mx_TextualEvent">{ text }</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -256,8 +256,6 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let position = 1;
|
||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
const completions = completionResult.completions.map((completion, i) => {
|
||||
|
@ -282,7 +280,7 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
return completions.length > 0 ? (
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
||||
<EmojiText element="div" className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</EmojiText>
|
||||
<div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
|
||||
{ completionResult.provider.renderCompletions(completions) }
|
||||
</div>
|
||||
) : null;
|
||||
|
|
|
@ -111,7 +111,6 @@ const EntityTile = React.createClass({
|
|||
let nameEl;
|
||||
const {name} = this.props;
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
if (!this.props.suppressOnHover) {
|
||||
const activeAgo = this.props.presenceLastActiveAgo ?
|
||||
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
|
||||
|
@ -128,24 +127,24 @@ const EntityTile = React.createClass({
|
|||
}
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
|
||||
<div className="mx_EntityTile_name" dir="auto">
|
||||
{ name }
|
||||
</EmojiText>
|
||||
</div>
|
||||
{presenceLabel}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.subtextLabel) {
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
|
||||
<div className="mx_EntityTile_name" dir="auto">
|
||||
{name}
|
||||
</EmojiText>
|
||||
</div>
|
||||
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
nameEl = (
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText>
|
||||
<div className="mx_EntityTile_name" dir="auto">{ name }</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import withMatrixClient from '../../../wrappers/withMatrixClient';
|
|||
import dis from '../../../dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
|
||||
|
@ -160,8 +161,11 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// show twelve hour timestamps
|
||||
isTwelveHour: PropTypes.bool,
|
||||
|
||||
// helper function to access relations for an event
|
||||
// helper function to access relations for this event
|
||||
getRelationsForEvent: PropTypes.func,
|
||||
|
||||
// whether to show reactions for this event
|
||||
showReactions: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -198,7 +202,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const client = this.props.matrixClient;
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
||||
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
|
||||
}
|
||||
},
|
||||
|
@ -223,7 +227,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const client = this.props.matrixClient;
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
||||
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
|
||||
}
|
||||
},
|
||||
|
@ -485,6 +489,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
getReactions() {
|
||||
if (
|
||||
!this.props.showReactions ||
|
||||
!this.props.getRelationsForEvent ||
|
||||
!SettingsStore.isFeatureEnabled("feature_reactions")
|
||||
) {
|
||||
|
@ -541,6 +546,50 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
||||
|
||||
// TLDR: Screen readers are complicated and can watch for new DOM elements, but not
|
||||
// changes to DOM elements. As such, we hack a bunch of conditions together.
|
||||
//
|
||||
// Screen readers do not react well to aria attributes changing dynamically after
|
||||
// parsing them. Although readers watch the DOM, they cannot react to aria-hidden
|
||||
// going from true to false. To work around that, we check to see if the eventSendStatus
|
||||
// is something worthwhile for us to read out. We specifically don't want to read
|
||||
// out pending/queued messages because they'll be read out again when they are sent.
|
||||
//
|
||||
// There's a small annoyance with doing this though: if we can't change the aria attrs,
|
||||
// we need to track the entry state for when the component mounts. As it stands, the
|
||||
// EventTile is unmounted/mounted when going pending->sent, and then a simple properties
|
||||
// change is made to mxEvent for sent->null (the final state). We abuse this cycle to
|
||||
// mute the pending state and react on the sent state.
|
||||
//
|
||||
// However there's then a bug where readers don't read messages from other people (they
|
||||
// enter the component as eventSendStatus of null) - to counteract this, we look for a
|
||||
// transaction_id under the unsigned object of the event. According to the spec, we can
|
||||
// use this to determine if an event was sent by us (as it's bound to the access token
|
||||
// which sent the event). This allows us to do a few checks on whether to speak:
|
||||
// * If the event was sent by our user ID and the eventSendStatus is 'sent', then speak.
|
||||
// We cannot check the transaction_id at this point because it is undefined. We can
|
||||
// make the assumption that 'sent' means this exact device is handling it though.
|
||||
// * If the event was sent by our user ID and the eventSendStatus is falsey (null), then
|
||||
// only speak if the event was not sent by us (no transaction_id).
|
||||
// * If the event was not sent by our user ID then speak.
|
||||
//
|
||||
// Note: although NVDA (a screen reader) does react to aria-hidden changing, it does so
|
||||
// in a horrible way. Because multiple properties and DOM elements are changing, it reads
|
||||
// the message twice when we limit the 'should speak' checks to just 'if eventSendStatus
|
||||
// is null'. This is part of the reason for the complexity above.
|
||||
//
|
||||
// Hopefully all of that leads to us not reading out messages in duplicate or triplicate.
|
||||
const sentByMyUserId = this.props.mxEvent.getSender() === MatrixClientPeg.get().getUserId();
|
||||
const sentByThisDevice = !!this.props.mxEvent.getUnsigned()["transaction_id"];
|
||||
let screenReaderShouldSpeak = false;
|
||||
if (!isSending) {
|
||||
if (this.props.eventSendStatus === 'sent') {
|
||||
screenReaderShouldSpeak = sentByMyUserId;
|
||||
} else if (!this.props.eventSendStatus) {
|
||||
screenReaderShouldSpeak = !sentByMyUserId || !sentByThisDevice;
|
||||
}
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
mx_EventTile: true,
|
||||
mx_EventTile_isEditing: this.props.isEditing,
|
||||
|
@ -597,9 +646,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
if (this.props.mxEvent.sender && avatarSize) {
|
||||
avatar = (
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={this.props.mxEvent.sender}
|
||||
<MemberAvatar
|
||||
member={this.props.mxEvent.sender}
|
||||
width={avatarSize} height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
aria-hidden={true} /* silence screen readers */
|
||||
buttonRole={null} /* trick screen readers into thinking this is not a button */
|
||||
tabIndex={null} /* trick screen readers into thinking this is not a button */
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -630,8 +683,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
onFocusChange={this.onActionBarFocusChange}
|
||||
/> : undefined;
|
||||
|
||||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||
const timestamp = this.props.mxEvent.getTs()
|
||||
? <MessageTimestamp
|
||||
showTwelveHour={this.props.isTwelveHour}
|
||||
ts={this.props.mxEvent.getTs()}
|
||||
ariaHidden={!screenReaderShouldSpeak}
|
||||
/> : null;
|
||||
|
||||
const keyRequestHelpText =
|
||||
<div className="mx_EventTile_keyRequestInfo_tooltip_contents">
|
||||
|
@ -678,14 +735,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</EmojiText>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
|
@ -770,13 +826,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
'replyThread',
|
||||
);
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={classes} aria-hidden={!screenReaderShouldSpeak}>
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
{ sender }
|
||||
<div className="mx_EventTile_line">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
<a href={permalink} onClick={this.onPermalinkClicked} aria-hidden={true}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
|
@ -794,7 +850,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ actionBar }
|
||||
</div>
|
||||
{
|
||||
// The avatar goes after the event tile as it's absolutly positioned to be over the
|
||||
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
||||
// the need for further z-indexing chaos)
|
||||
}
|
||||
|
|
|
@ -978,7 +978,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
let backButton;
|
||||
if (this.props.member.roomId) {
|
||||
|
@ -993,7 +992,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<div className="mx_MemberInfo_name">
|
||||
{ backButton }
|
||||
{ e2eIconElement }
|
||||
<EmojiText element="h2">{ memberName }</EmojiText>
|
||||
<h2>{ memberName }</h2>
|
||||
</div>
|
||||
{ avatarElement }
|
||||
<div className="mx_MemberInfo_container">
|
||||
|
|
|
@ -40,7 +40,6 @@ import Analytics from '../../../Analytics';
|
|||
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
import * as RichText from '../../../RichText';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||
|
@ -51,10 +50,9 @@ import ContentMessages from '../../../ContentMessages';
|
|||
|
||||
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
|
||||
|
||||
import {
|
||||
asciiRegexp, unicodeRegexp, shortnameToUnicode,
|
||||
asciiList, mapUnicodeToShort, toShort,
|
||||
} from 'emojione';
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import {makeUserPermalink} from "../../../matrix-to";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
|
@ -63,9 +61,7 @@ import ReplyThread from "../elements/ReplyThread";
|
|||
import {ContentHelpers} from 'matrix-js-sdk';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
|
@ -273,9 +269,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
case 'emoji':
|
||||
// XXX: apparently you can't return plain strings from serializer rules
|
||||
// until https://github.com/ianstormtaylor/slate/pull/1854 is merged.
|
||||
// So instead we temporarily wrap emoji from RTE in an arbitrary tag
|
||||
// (<b/>). <span/> would be nicer, but in practice it causes CSS issues.
|
||||
return <b>{ obj.data.get('emojiUnicode') }</b>;
|
||||
// So instead we temporarily wrap emoji from RTE in a span.
|
||||
return <span>{ obj.data.get('emojiUnicode') }</span>;
|
||||
}
|
||||
return this.renderNode({
|
||||
node: obj,
|
||||
|
@ -375,7 +370,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
|
||||
forComposerQuote: true,
|
||||
returnString: true,
|
||||
emojiOne: false,
|
||||
});
|
||||
const fragment = this.html.deserialize(html);
|
||||
// FIXME: do we want to put in a permalink to the original quote here?
|
||||
|
@ -538,17 +532,15 @@ export default class MessageComposerInput extends React.Component {
|
|||
// Automatic replacement of plaintext emoji to Unicode emoji
|
||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||
// The first matched group includes just the matched plaintext emoji
|
||||
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if (emojiMatch) {
|
||||
// plaintext -> hex unicode
|
||||
const emojiUc = asciiList[emojiMatch[1]];
|
||||
// hex unicode -> shortname -> actual unicode
|
||||
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
|
||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if (emoticonMatch) {
|
||||
const data = EMOJIBASE.find(e => e.emoticon === emoticonMatch[1]);
|
||||
const unicodeEmoji = data ? data.unicode : '';
|
||||
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - emojiMatch[1].length - 1,
|
||||
offset: currentStartOffset - emoticonMatch[1].length - 1,
|
||||
},
|
||||
focus: {
|
||||
key: editorState.startText.key,
|
||||
|
@ -561,54 +553,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// emojioneify any emoji
|
||||
let foundEmoji;
|
||||
do {
|
||||
foundEmoji = false;
|
||||
|
||||
for (const node of editorState.document.getTexts()) {
|
||||
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
|
||||
let match;
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: node.key,
|
||||
offset: match.index,
|
||||
},
|
||||
focus: {
|
||||
key: node.key,
|
||||
offset: match.index + match[0].length,
|
||||
},
|
||||
});
|
||||
const inline = Inline.create({
|
||||
type: 'emoji',
|
||||
data: { emojiUnicode: match[0] },
|
||||
});
|
||||
change = change.insertInlineAtRange(range, inline);
|
||||
editorState = change.value;
|
||||
|
||||
// if we replaced an emoji, start again looking for more
|
||||
// emoji in the new editor state since doing the replacement
|
||||
// will change the node structure & offsets so we can't compute
|
||||
// insertion ranges from node.key / match.index anymore.
|
||||
foundEmoji = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (foundEmoji);
|
||||
|
||||
// work around weird bug where inserting emoji via the macOS
|
||||
// emoji picker can leave the selection stuck in the emoji's
|
||||
// child text. This seems to happen due to selection getting
|
||||
// moved in the normalisation phase after calculating these changes
|
||||
if (editorState.selection.anchor.key &&
|
||||
editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') {
|
||||
change = change.moveToStartOfNextText();
|
||||
editorState = change.value;
|
||||
}
|
||||
|
||||
if (this.props.onInputStateChanged && editorState.blocks.size > 0) {
|
||||
let blockType = editorState.blocks.first().type;
|
||||
// console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks);
|
||||
|
@ -1295,7 +1239,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
// Move selection to the end of the selected history
|
||||
const change = editorState.change().moveToEndOfNode(editorState.document);
|
||||
|
||||
// We don't call this.onChange(change) now, as fixups on stuff like emoji
|
||||
// We don't call this.onChange(change) now, as fixups on stuff like pills
|
||||
// should already have been done and persisted in the history.
|
||||
editorState = change.value;
|
||||
|
||||
|
@ -1475,17 +1419,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
case 'emoji': {
|
||||
const { data } = node;
|
||||
const emojiUnicode = data.get('emojiUnicode');
|
||||
const uri = RichText.unicodeToEmojiUri(emojiUnicode);
|
||||
const shortname = toShort(emojiUnicode);
|
||||
const className = classNames('mx_emojione', {
|
||||
mx_emojione_selected: isSelected,
|
||||
});
|
||||
const style = {};
|
||||
if (props.selected) style.border = '1px solid blue';
|
||||
return <img className={ className } src={ uri }
|
||||
title={ shortname } alt={ emojiUnicode } style={style}
|
||||
/>;
|
||||
return data.get('emojiUnicode');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -211,11 +211,13 @@ module.exports = React.createClass({
|
|||
<MemberAvatar
|
||||
member={this.props.member}
|
||||
fallbackUserId={this.props.fallbackUserId}
|
||||
aria-hidden="true"
|
||||
width={14} height={14} resizeMethod="crop"
|
||||
style={style}
|
||||
title={title}
|
||||
onClick={this.props.onClick}
|
||||
aria-hidden={true} /* silence screen readers */
|
||||
buttonRole={null} /* trick screen readers into thinking this is not a button */
|
||||
tabIndex={null} /* trick screen readers into thinking this is not a button */
|
||||
/>
|
||||
</Velociraptor>
|
||||
);
|
||||
|
|
|
@ -66,13 +66,12 @@ export default class ReplyPreview extends React.Component {
|
|||
if (!this.state.event) return null;
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
return <div className="mx_ReplyPreview">
|
||||
<div className="mx_ReplyPreview_section">
|
||||
<EmojiText element="div" className="mx_ReplyPreview_header mx_ReplyPreview_title">
|
||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_title">
|
||||
{ '💬 ' + _t('Replying') }
|
||||
</EmojiText>
|
||||
</div>
|
||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
|
||||
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
|
||||
onClick={cancelQuoting} />
|
||||
|
|
|
@ -147,7 +147,6 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
let searchStatus = null;
|
||||
let cancelButton = null;
|
||||
|
@ -191,10 +190,10 @@ module.exports = React.createClass({
|
|||
roomName = this.props.room.name;
|
||||
}
|
||||
|
||||
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const name =
|
||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||
<EmojiText dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
|
||||
<div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>
|
||||
{ searchStatus }
|
||||
</div>;
|
||||
|
||||
|
|
|
@ -750,6 +750,7 @@ module.exports = React.createClass({
|
|||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})},
|
||||
addRoomLabel: _t("Start chat"),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.recent'],
|
||||
|
|
|
@ -342,7 +342,6 @@ module.exports = React.createClass({
|
|||
badge = <div className={badgeClasses}>{ badgeContent }</div>;
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
let label;
|
||||
let subtextLabel;
|
||||
let tooltip;
|
||||
|
@ -354,14 +353,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
||||
|
||||
if (this.state.selected) {
|
||||
const nameSelected = <EmojiText>{ name }</EmojiText>;
|
||||
|
||||
label = <div title={name} className={nameClasses} dir="auto">{ nameSelected }</div>;
|
||||
} else {
|
||||
label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>;
|
||||
}
|
||||
label = <div title={name} className={nameClasses} dir="auto">{ name }</div>;
|
||||
} else if (this.state.hover) {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import WhoIsTyping from '../../../WhoIsTyping';
|
||||
import Timer from '../../../utils/Timer';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
@ -212,15 +211,13 @@ module.exports = React.createClass({
|
|||
return (<div className="mx_WhoIsTypingTile_empty" />);
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
return (
|
||||
<li className="mx_WhoIsTypingTile">
|
||||
<div className="mx_WhoIsTypingTile_avatars">
|
||||
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
<div className="mx_WhoIsTypingTile_label">
|
||||
<EmojiText>{ typingString }</EmojiText>
|
||||
{ typingString }
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -174,14 +174,13 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
} else if (this.state.loading) {
|
||||
return <Spinner />;
|
||||
} else if (this.state.backupInfo) {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
let clientBackupStatus;
|
||||
let restoreButtonCaption = _t("Restore from Backup");
|
||||
|
||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
clientBackupStatus = <div>
|
||||
<p>{encryptedMessageAreEncrypted}</p>
|
||||
<p>{_t("This device is backing up your keys. ")}<EmojiText>✅</EmojiText></p>
|
||||
<p>✅ {_t("This device is backing up your keys. ")}</p>
|
||||
</div>;
|
||||
} else {
|
||||
clientBackupStatus = <div>
|
||||
|
|
|
@ -36,7 +36,6 @@ export default class VerificationShowSas extends React.Component {
|
|||
|
||||
render() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let sasDisplay;
|
||||
let sasCaption;
|
||||
|
@ -44,7 +43,7 @@ export default class VerificationShowSas extends React.Component {
|
|||
const emojiBlocks = this.props.sas.emoji.map(
|
||||
(emoji, i) => <div className="mx_VerificationShowSas_emojiSas_block" key={i}>
|
||||
<div className="mx_VerificationShowSas_emojiSas_emoji">
|
||||
<EmojiText addAlt={false}>{emoji[0]}</EmojiText>
|
||||
{ emoji[0] }
|
||||
</div>
|
||||
<div className="mx_VerificationShowSas_emojiSas_label">
|
||||
{_t(capFirst(emoji[1]))}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,11 +18,12 @@ limitations under the License.
|
|||
import {UserPillPart, RoomPillPart, PlainPart} from "./parts";
|
||||
|
||||
export default class AutocompleteWrapperModel {
|
||||
constructor(updateCallback, getAutocompleterComponent, updateQuery) {
|
||||
constructor(updateCallback, getAutocompleterComponent, updateQuery, room) {
|
||||
this._updateCallback = updateCallback;
|
||||
this._getAutocompleterComponent = getAutocompleterComponent;
|
||||
this._updateQuery = updateQuery;
|
||||
this._query = null;
|
||||
this._room = room;
|
||||
}
|
||||
|
||||
onEscape(e) {
|
||||
|
@ -83,7 +85,8 @@ export default class AutocompleteWrapperModel {
|
|||
case "@": {
|
||||
const displayName = completion.completion;
|
||||
const userId = completion.completionId;
|
||||
return new UserPillPart(userId, displayName);
|
||||
const member = this._room.getMember(userId);
|
||||
return new UserPillPart(userId, displayName, member);
|
||||
}
|
||||
case "#": {
|
||||
const displayAlias = completion.completionId;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,7 +18,7 @@ limitations under the License.
|
|||
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
|
||||
import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts";
|
||||
|
||||
function parseHtmlMessage(html) {
|
||||
function parseHtmlMessage(html, room) {
|
||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||
// no nodes from parsing here should be inserted in the document,
|
||||
// as scripts in event handlers, etc would be executed then.
|
||||
|
@ -37,8 +38,8 @@ function parseHtmlMessage(html) {
|
|||
const resourceId = pillMatch[1]; // The room/user ID
|
||||
const prefix = pillMatch[2]; // The first character of prefix
|
||||
switch (prefix) {
|
||||
case "@": return new UserPillPart(resourceId, n.textContent);
|
||||
case "#": return new RoomPillPart(resourceId, n.textContent);
|
||||
case "@": return new UserPillPart(resourceId, n.textContent, room.getMember(resourceId));
|
||||
case "#": return new RoomPillPart(resourceId);
|
||||
default: return new PlainPart(n.textContent);
|
||||
}
|
||||
}
|
||||
|
@ -54,10 +55,10 @@ function parseHtmlMessage(html) {
|
|||
return parts;
|
||||
}
|
||||
|
||||
export function parseEvent(event) {
|
||||
export function parseEvent(event, room) {
|
||||
const content = event.getContent();
|
||||
if (content.format === "org.matrix.custom.html") {
|
||||
return parseHtmlMessage(content.formatted_body || "");
|
||||
return parseHtmlMessage(content.formatted_body || "", room);
|
||||
} else {
|
||||
const body = content.body || "";
|
||||
const lines = body.split("\n");
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import AutocompleteWrapperModel from "./autocomplete";
|
||||
import Avatar from "../Avatar";
|
||||
import MatrixClientPeg from "../MatrixClientPeg";
|
||||
|
||||
class BasePart {
|
||||
constructor(text = "") {
|
||||
|
@ -150,21 +153,21 @@ class PillPart extends BasePart {
|
|||
|
||||
toDOMNode() {
|
||||
const container = document.createElement("span");
|
||||
container.className = this.type;
|
||||
container.className = this.className;
|
||||
container.appendChild(document.createTextNode(this.text));
|
||||
this.setAvatar(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
updateDOMNode(node) {
|
||||
const textNode = node.childNodes[0];
|
||||
if (textNode.textContent !== this.text) {
|
||||
// console.log("changing pill text from", textNode.textContent, "to", this.text);
|
||||
textNode.textContent = this.text;
|
||||
}
|
||||
if (node.className !== this.type) {
|
||||
// console.log("turning", node.className, "into", this.type);
|
||||
node.className = this.type;
|
||||
if (node.className !== this.className) {
|
||||
node.className = this.className;
|
||||
}
|
||||
this.setAvatar(node);
|
||||
}
|
||||
|
||||
canUpdateDOMNode(node) {
|
||||
|
@ -174,6 +177,20 @@ class PillPart extends BasePart {
|
|||
node.childNodes[0].nodeType === Node.TEXT_NODE;
|
||||
}
|
||||
|
||||
// helper method for subclasses
|
||||
_setAvatarVars(node, avatarUrl, initialLetter) {
|
||||
const avatarBackground = `url('${avatarUrl}')`;
|
||||
const avatarLetter = `'${initialLetter}'`;
|
||||
// check if the value is changing,
|
||||
// otherwise the avatars flicker on every keystroke while updating.
|
||||
if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) {
|
||||
node.style.setProperty("--avatar-background", avatarBackground);
|
||||
}
|
||||
if (node.style.getPropertyValue("--avatar-letter") !== avatarLetter) {
|
||||
node.style.setProperty("--avatar-letter", avatarLetter);
|
||||
}
|
||||
}
|
||||
|
||||
get canEdit() {
|
||||
return false;
|
||||
}
|
||||
|
@ -218,17 +235,71 @@ export class NewlinePart extends BasePart {
|
|||
export class RoomPillPart extends PillPart {
|
||||
constructor(displayAlias) {
|
||||
super(displayAlias, displayAlias);
|
||||
this._room = this._findRoomByAlias(displayAlias);
|
||||
}
|
||||
|
||||
_findRoomByAlias(alias) {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (alias[0] === '#') {
|
||||
return client.getRooms().find((r) => {
|
||||
return r.getAliases().includes(alias);
|
||||
});
|
||||
} else {
|
||||
return client.getRoom(alias);
|
||||
}
|
||||
}
|
||||
|
||||
setAvatar(node) {
|
||||
let initialLetter = "";
|
||||
let avatarUrl = Avatar.avatarUrlForRoom(this._room, 16 * window.devicePixelRatio, 16 * window.devicePixelRatio);
|
||||
if (!avatarUrl) {
|
||||
initialLetter = Avatar.getInitialLetter(this._room.name);
|
||||
avatarUrl = `../../${Avatar.defaultAvatarUrlForString(this._room.roomId)}`;
|
||||
}
|
||||
this._setAvatarVars(node, avatarUrl, initialLetter);
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "room-pill";
|
||||
}
|
||||
|
||||
get className() {
|
||||
return "mx_RoomPill mx_Pill";
|
||||
}
|
||||
}
|
||||
|
||||
export class UserPillPart extends PillPart {
|
||||
constructor(userId, displayName, member) {
|
||||
super(userId, displayName);
|
||||
this._member = member;
|
||||
}
|
||||
|
||||
setAvatar(node) {
|
||||
const name = this._member.name || this._member.userId;
|
||||
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this._member.userId);
|
||||
let avatarUrl = Avatar.avatarUrlForMember(
|
||||
this._member,
|
||||
16 * window.devicePixelRatio,
|
||||
16 * window.devicePixelRatio);
|
||||
let initialLetter = "";
|
||||
if (avatarUrl === defaultAvatarUrl) {
|
||||
// the url from defaultAvatarUrlForString is meant to go in an img element,
|
||||
// which has the base of the document. we're using it in css,
|
||||
// which has the base of the theme css file, two levels deeper than the document,
|
||||
// so go up to the level of the document.
|
||||
avatarUrl = `../../${avatarUrl}`;
|
||||
initialLetter = Avatar.getInitialLetter(name);
|
||||
}
|
||||
this._setAvatarVars(node, avatarUrl, initialLetter);
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "user-pill";
|
||||
}
|
||||
|
||||
get className() {
|
||||
return "mx_UserPill mx_Pill";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -256,9 +327,14 @@ export class PillCandidatePart extends PlainPart {
|
|||
}
|
||||
|
||||
export class PartCreator {
|
||||
constructor(getAutocompleterComponent, updateQuery) {
|
||||
constructor(getAutocompleterComponent, updateQuery, room) {
|
||||
this._autoCompleteCreator = (updateCallback) => {
|
||||
return new AutocompleteWrapperModel(updateCallback, getAutocompleterComponent, updateQuery);
|
||||
return new AutocompleteWrapperModel(
|
||||
updateCallback,
|
||||
getAutocompleterComponent,
|
||||
updateQuery,
|
||||
room,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -1,3 +1,20 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export function htmlSerialize(model) {
|
||||
return model.parts.reduce((html, part) => {
|
||||
switch (part.type) {
|
||||
|
|
|
@ -799,6 +799,7 @@
|
|||
"Invites": "Invites",
|
||||
"Favourites": "Favourites",
|
||||
"People": "People",
|
||||
"Start chat": "Start chat",
|
||||
"Rooms": "Rooms",
|
||||
"Low priority": "Low priority",
|
||||
"Historical": "Historical",
|
||||
|
@ -1053,12 +1054,12 @@
|
|||
"%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar",
|
||||
"collapse": "collapse",
|
||||
"expand": "expand",
|
||||
"Edit message": "Edit message",
|
||||
"Power level": "Power level",
|
||||
"Custom level": "Custom level",
|
||||
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
|
||||
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
||||
"Room directory": "Room directory",
|
||||
"Start chat": "Start chat",
|
||||
"And %(count)s more...|other": "And %(count)s more...",
|
||||
"ex. @bob:example.com": "ex. @bob:example.com",
|
||||
"Add User": "Add User",
|
||||
|
@ -1475,6 +1476,7 @@
|
|||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||
"Active call": "Active call",
|
||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||
"Add room": "Add room",
|
||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||
"Search failed": "Search failed",
|
||||
|
@ -1495,6 +1497,7 @@
|
|||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||
"Failed to load timeline position": "Failed to load timeline position",
|
||||
"Guest": "Guest",
|
||||
"Your profile": "Your profile",
|
||||
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
|
||||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||
|
|
|
@ -1392,5 +1392,79 @@
|
|||
"Okay": "Bone",
|
||||
"Success!": "Sukceso!",
|
||||
"Retry": "Reprovi",
|
||||
"Set up": "Agordi"
|
||||
"Set up": "Agordi",
|
||||
"A conference call could not be started because the integrations server is not available": "Grupa voko ne povis komenciĝi, ĉar la kuniga servilo estas neatingebla",
|
||||
"Replying With Files": "Respondado kun dosieroj",
|
||||
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Nun ne eblas respondi kun dosiero. Ĉu vi volas alŝuti la dosieron sen respondo?",
|
||||
"The file '%(fileName)s' failed to upload.": "Malsukcesis alŝuti dosieron «%(fileName)s».",
|
||||
"The server does not support the room version specified.": "La servilo ne subtenas la donitan ĉambran version.",
|
||||
"Name or Matrix ID": "Nomo aŭ Matrix-identigilo",
|
||||
"Email, name or Matrix ID": "Retpoŝtadreso, nomo, aŭ Matrix-identigilo",
|
||||
"Upgrades a room to a new version": "Gradaltigas ĉambron al nova versio",
|
||||
"Room upgrade confirmation": "Konfirmo de ĉambra gradaltigo",
|
||||
"Upgrading a room can be destructive and isn't always necessary.": "Gradaltigo de ĉambro povas esti detrua kaj ne estas ĉiam necesa.",
|
||||
"Room upgrades are usually recommended when a room version is considered <i>unstable</i>. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Gradaltigoj de ĉambroj estas kutime rekomendataj kiam ĉambra versio estas opiniata <i>malstabila</i>. Malstabilaj ĉambraj versioj povas kunhavi erarojn, mankojn de funkcioj, aŭ malsekuraĵojn.",
|
||||
"Room upgrades usually only affect <i>server-side</i> processing of the room. If you're having problems with your Riot client, please file an issue with <issueLink />.": "Ĉambraj gradaltigoj efikas nur sur <i>servil-flanka</i> funkciado de la ĉambro. Se vi havas problemon kun via kliento (Riot), bonvolu raparti problemon per <issueLink />.",
|
||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Averto</b>: Gradaltigo de ĉambro <i>ne transmetos ĉiujn ĉambranojn al la nova versio de la ĉambro.</i> Ni afiŝos ligilon al la nova ĉambro en la malnova versio de la ĉambro – ĉambranoj devos tien klaki por aliĝi al la nova ĉambro.",
|
||||
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Bonvolu konfirmi, ke vi certe volas gradaltigi ĉi tiun ĉambron de <oldVersion /> al <newVersion />.",
|
||||
"Upgrade": "Gradaltigi",
|
||||
"Changes your display nickname in the current room only": "Ŝanĝas vian vidigan nomon nur en la nuna ĉambro",
|
||||
"Changes your avatar in this current room only": "Ŝanĝas vian profilbildon nur en la nuna ĉambro",
|
||||
"Gets or sets the room topic": "Ekhavas aŭ agordas la temon de la ĉambro",
|
||||
"This room has no topic.": "Ĉi tiu ĉambro ne havas temon.",
|
||||
"Sets the room name": "Agordas nomon de la ĉambro",
|
||||
"Sends the given message coloured as a rainbow": "Sendas la mesaĝon ĉielarke kolorigitan",
|
||||
"Sends the given emote coloured as a rainbow": "Sendas la mienon ĉielarke kolorigitan",
|
||||
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s gradaltigis ĉi tiun ĉambron.",
|
||||
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s publikigis la ĉambron al kiu ajn konas la ligilon.",
|
||||
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s necesigis invitojn por aliĝoj al la ĉambro.",
|
||||
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s ŝanĝis la aliĝan regulon al %(rule)s",
|
||||
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s permesis al gastoj aliĝi al la ĉambro.",
|
||||
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s malpermesis al gastoj aliĝi al la ĉambro.",
|
||||
"Unbans user with given ID": "Malforbaras uzanton kun la donita identigilo",
|
||||
"Please supply a https:// or http:// widget URL": "Bonvolu doni URL-on de fenestraĵo kun https:// aŭ http://",
|
||||
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s ŝanĝis aliron de gastoj al %(rule)s",
|
||||
"%(displayName)s is typing …": "%(displayName)s tajpas…",
|
||||
"%(names)s and %(count)s others are typing …|other": "%(names)s kaj %(count)s aliaj tajpas…",
|
||||
"%(names)s and %(count)s others are typing …|one": "%(names)s kaj unu alia tajpas…",
|
||||
"%(names)s and %(lastPerson)s are typing …": "%(names)s kaj %(lastPerson)s tajpas…",
|
||||
"Unrecognised address": "Nerekonita adreso",
|
||||
"User %(userId)s is already in the room": "Uzanto %(userId)s jam enas la ĉambron",
|
||||
"User %(user_id)s may or may not exist": "Uzanto %(user_id)s eble ne ekzistas",
|
||||
"The user must be unbanned before they can be invited.": "Necesas malforbari ĉi tiun uzanton antaŭ ol ĝin inviti.",
|
||||
"The user's homeserver does not support the version of the room.": "Hejmservilo de ĉi tiu uzanto ne subtenas la version de la ĉambro.",
|
||||
"No need for symbols, digits, or uppercase letters": "Ne necesas simboloj, cirefoj, aŭ majuskloj",
|
||||
"Render simple counters in room header": "Bildigi simplajn kalkulilojn en la ĉapo de la fenestro",
|
||||
"Edit messages after they have been sent (refresh to apply changes)": "Redakti mesaĝojn senditajn (aktualigu por apliki ŝanĝojn)",
|
||||
"React to messages with emoji (refresh to apply changes)": "Reagi al mesaĝoj per bildsignoj (aktualigu por apliki ŝanĝojn)",
|
||||
"Enable Emoji suggestions while typing": "Ŝalti proponojn de bildsignoj dum tajpado",
|
||||
"Show a placeholder for removed messages": "Meti kovrilon anstataŭ forigitajn mesaĝojn",
|
||||
"Show join/leave messages (invites/kicks/bans unaffected)": "Montri mesaĝojn pri aliĝo/foriro (neteme pri invitoj/forpeloj/forbaroj)",
|
||||
"Show avatar changes": "Montri ŝanĝojn de profilbildoj",
|
||||
"Show display name changes": "Montri ŝanĝojn de vidigaj nomoj",
|
||||
"Show read receipts sent by other users": "Montri legokonfirmojn senditajn de aliaj uzantoj",
|
||||
"Show avatars in user and room mentions": "Montri profilbildojn en mencioj de uzantoj kaj ĉambroj",
|
||||
"Enable big emoji in chat": "Ŝalti grandajn bildsignojn en babilejo",
|
||||
"Send typing notifications": "Sendi sciigojn pri tajpado",
|
||||
"Allow Peer-to-Peer for 1:1 calls": "Permesi samtavolan teĥnikon por duopaj vokoj",
|
||||
"Prompt before sending invites to potentially invalid matrix IDs": "Averti antaŭ ol sendi invitojn al eble nevalidaj Matrix-identigiloj",
|
||||
"Order rooms in the room list by most important first instead of most recent": "Ordigi ĉambrojn en listo de ĉambroj laŭ graveco anstataŭ freŝeco",
|
||||
"Messages containing my username": "Mesaĝoj kun mia salutnomo",
|
||||
"When rooms are upgraded": "Kiam ĉambroj gradaltiĝas",
|
||||
"The other party cancelled the verification.": "La alia kontrolano nuligis la kontrolon.",
|
||||
"You've successfully verified this user.": "Vi sukcese kontrolis ĉi tiun uzanton.",
|
||||
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sekuraj mesaĝoj kun ĉi tiu uzanto estas tutvoje ĉirfitaj kaj nelegeblaj al ceteruloj.",
|
||||
"Verify this user by confirming the following emoji appear on their screen.": "Kontrolu ĉi tiun uzanton per konfirmo, ke la jenaj bildsignoj aperis sur ĝia ekrano.",
|
||||
"Verify this user by confirming the following number appears on their screen.": "Kontrolu ĉu tiun uzanton per konfirmo, ke la jena numero aperis sur ĝia ekrano.",
|
||||
"Unable to find a supported verification method.": "Ne povas trovi subtenatan metodon de kontrolo.",
|
||||
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "Por la plej bona sekureco, ni rekomendas fari ĉi tion persone, aŭ per alia, fidata komunikilo.",
|
||||
"Santa": "Kristnaska viro",
|
||||
"Thumbs up": "Dikfingro supren",
|
||||
"Paperclip": "Paperkuntenilo",
|
||||
"Pin": "Pinglo",
|
||||
"Your homeserver does not support device management.": "Via hejmservilo ne subtenas administradon de aparatoj.",
|
||||
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Ni sendis al vi retleteron por konfirmi vian adreson. Bonvolu sekvi la tieajn intrukciojn kaj poste klaki al la butono sube.",
|
||||
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Ĉu vi certas? Vi perdos ĉiujn viajn ĉifritajn mesaĝojn, se viaj ŝlosiloj ne estas savkopiitaj.",
|
||||
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Ĉifritaj mesaĝoj estas sekurigitaj per tutvoja ĉifrado. Nur vi kaj la ricevonto(j) havas la ŝlosilojn necesajn por legado.",
|
||||
"This device is backing up your keys. ": "Ĉi tiu aparato savkopias viajn ŝlosilojn. "
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"Default Device": "Oletuslaite",
|
||||
"Microphone": "Mikrofoni",
|
||||
"Camera": "Kamera",
|
||||
"Advanced": "Kehittyneet",
|
||||
"Advanced": "Edistynyt",
|
||||
"Algorithm": "Algoritmi",
|
||||
"Hide removed messages": "Piilota poistetut viestit",
|
||||
"Always show message timestamps": "Näytä aina viestien aikaleimat",
|
||||
|
@ -70,7 +70,7 @@
|
|||
"and %(count)s others...|one": "ja yksi muu...",
|
||||
"Ban": "Anna porttikielto",
|
||||
"Banned users": "Porttikiellon saanneet käyttäjät",
|
||||
"Bans user with given id": "Antaa porttikiellon käyttäjälle jolla on annettu tunniste",
|
||||
"Bans user with given id": "Antaa porttikiellon tunnuksen mukaiselle käyttäjälle",
|
||||
"Bulk Options": "Bulkkiasetukset",
|
||||
"Changes your display nickname": "Muuttaa näyttönimesi",
|
||||
"Changes colour scheme of current room": "Muuttaa tamänhetkisen huoneen väritystä",
|
||||
|
@ -175,7 +175,7 @@
|
|||
"Incoming call from %(name)s": "Saapuva puhelu käyttäjältä %(name)s",
|
||||
"Incoming video call from %(name)s": "Saapuva videopuhelu käyttäjältä %(name)s",
|
||||
"Incoming voice call from %(name)s": "Saapuva äänipuhelu käyttäjältä %(name)s",
|
||||
"Incorrect username and/or password.": "Virheellinen käyttäjänimi ja/tai salasana.",
|
||||
"Incorrect username and/or password.": "Virheellinen käyttäjätunnus ja/tai salasana.",
|
||||
"Incorrect verification code": "Virheellinen varmennuskoodi",
|
||||
"Integrations Error": "Integraatiovirhe",
|
||||
"Interface Language": "Käyttöliittymän kieli",
|
||||
|
@ -185,13 +185,13 @@
|
|||
"Invite new room members": "Kutsu lisää jäseniä huoneeseen",
|
||||
"Invited": "Kutsuttu",
|
||||
"Invites": "Kutsut",
|
||||
"Invites user with given id to current room": "Kutsuu annetun käyttäjätunnisteen mukaisen käyttäjän huoneeseen",
|
||||
"Invites user with given id to current room": "Kutsuu tunnuksen mukaisen käyttäjän huoneeseen",
|
||||
"Sign in with": "Tunnistus",
|
||||
"Join Room": "Liity huoneeseen",
|
||||
"Joins room with given alias": "Liittyy huoneeseen jolla on annettu alias",
|
||||
"Jump to first unread message.": "Hyppää ensimmäiseen lukemattomaan viestiin.",
|
||||
"Kick": "Poista huoneesta",
|
||||
"Kicks user with given id": "Poistaa käyttäjätunnisteen mukaisen käyttäjän huoneesta",
|
||||
"Kicks user with given id": "Poistaa tunnuksen mukaisen käyttäjän huoneesta",
|
||||
"Labs": "Laboratorio",
|
||||
"Last seen": "Viimeksi nähty",
|
||||
"Leave room": "Poistu huoneesta",
|
||||
|
@ -217,8 +217,8 @@
|
|||
"not specified": "ei määritetty",
|
||||
"(not supported by this browser)": "(ei tuettu tässä selaimessa)",
|
||||
"<not supported>": "<ei tuettu>",
|
||||
"AM": "AM",
|
||||
"PM": "PM",
|
||||
"AM": "ap.",
|
||||
"PM": "ip.",
|
||||
"NOT verified": "EI varmennettu",
|
||||
"NOTE: Apps are not end-to-end encrypted": "Huom: Ohjelmat eivät käytä osapuolten välistä salausta",
|
||||
"No display name": "Ei näyttönimeä",
|
||||
|
@ -328,7 +328,7 @@
|
|||
"Turn Markdown off": "Ota Markdown pois käytöstä",
|
||||
"Turn Markdown on": "Ota Markdown käyttöön",
|
||||
"%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s otti osapuolten välisen salauksen käyttöön (algoritmi %(algorithm)s).",
|
||||
"Username invalid: %(errMessage)s": "Virheellinen käyttäjänimi: %(errMessage)s",
|
||||
"Username invalid: %(errMessage)s": "Käyttäjätunnus ei kelpaa: %(errMessage)s",
|
||||
"Users": "Käyttäjät",
|
||||
"Verification": "Varmennus",
|
||||
"verified": "varmennettu",
|
||||
|
@ -370,13 +370,13 @@
|
|||
"Your password has been reset": "Salasanasi on palautettu",
|
||||
"You should not yet trust it to secure data": "Sinun ei vielä kannata luottaa siihen turvataksesi dataa",
|
||||
"Your home server does not support device management.": "Kotipalvelimesi ei tue laitteiden hallintaa.",
|
||||
"Sun": "Su",
|
||||
"Mon": "Ma",
|
||||
"Tue": "Ti",
|
||||
"Wed": "Ke",
|
||||
"Thu": "To",
|
||||
"Fri": "Pe",
|
||||
"Sat": "La",
|
||||
"Sun": "su",
|
||||
"Mon": "ma",
|
||||
"Tue": "ti",
|
||||
"Wed": "ke",
|
||||
"Thu": "to",
|
||||
"Fri": "pe",
|
||||
"Sat": "la",
|
||||
"Set a display name:": "Aseta näyttönimi:",
|
||||
"This server does not support authentication with a phone number.": "Tämä palvelin ei tue autentikointia puhelinnumeron avulla.",
|
||||
"Missing password.": "Salasana puuttuu.",
|
||||
|
@ -502,7 +502,7 @@
|
|||
"Usage": "Käyttö",
|
||||
"Use compact timeline layout": "Käytä tiivistä aikajanaa",
|
||||
"Use with caution": "Käytä varoen",
|
||||
"User ID": "Käyttäjätunniste",
|
||||
"User ID": "Käyttäjätunnus",
|
||||
"User Interface": "Käyttöliittymä",
|
||||
"User name": "Käyttäjänimi",
|
||||
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s asetti aiheeksi \"%(topic)s\".",
|
||||
|
@ -520,7 +520,7 @@
|
|||
"Server unavailable, overloaded, or something else went wrong.": "Palvelin on saavuttamattomissa, ylikuormitettu tai jotain muuta meni vikaan.",
|
||||
"The email address linked to your account must be entered.": "Sinun pitää syöttää tiliisi liitetty sähköpostiosoite.",
|
||||
"The visibility of existing history will be unchanged": "Olemassaolevan viestihistorian näkyvyys ei muutu",
|
||||
"To get started, please pick a username!": "Valitse käyttäjänimi aloittaaksesi!",
|
||||
"To get started, please pick a username!": "Aloita valitsemalla käyttäjätunnus!",
|
||||
"To use it, just wait for autocomplete results to load and tab through them.": "Käyttääksesi sitä odota vain automaattitäydennyksiä ja selaa niiden läpi.",
|
||||
"To reset your password, enter the email address linked to your account": "Syötä tiliisi liitetty sähköpostiosoite uudelleenalustaaksesi salasanasi",
|
||||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Aikajanan tietty hetki yritettiin ladata, mutta sinulla ei ole oikeutta nähdä kyseistä viestiä.",
|
||||
|
@ -538,18 +538,18 @@
|
|||
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Salasanan muuttaminen onnistui. Et saa push-ilmoituksia muilla laitteilla ennen kuin kirjaudut niihin sisään",
|
||||
"You seem to be in a call, are you sure you want to quit?": "Sinulla näyttää olevan puhelu kesken. Haluatko varmasti lopettaa?",
|
||||
"You seem to be uploading files, are you sure you want to quit?": "Näytät lataavan tiedostoja. Oletko varma että haluat lopettaa?",
|
||||
"Jan": "tammikuu",
|
||||
"Feb": "helmikuu",
|
||||
"Mar": "maaliskuu",
|
||||
"Apr": "huhtikuu",
|
||||
"May": "toukokuu",
|
||||
"Jun": "kesäkuu",
|
||||
"Jul": "heinäkuu",
|
||||
"Aug": "elokuu",
|
||||
"Sep": "syyskuu",
|
||||
"Oct": "lokakuu",
|
||||
"Nov": "marraskuu",
|
||||
"Dec": "joulukuu",
|
||||
"Jan": "tammi",
|
||||
"Feb": "helmi",
|
||||
"Mar": "maalis",
|
||||
"Apr": "huhti",
|
||||
"May": "touko",
|
||||
"Jun": "kesä",
|
||||
"Jul": "heinä",
|
||||
"Aug": "elo",
|
||||
"Sep": "syys",
|
||||
"Oct": "loka",
|
||||
"Nov": "marras",
|
||||
"Dec": "joulu",
|
||||
"User names may only contain letters, numbers, dots, hyphens and underscores.": "Käyttäjänimet voivat sisältää vain kirjaimia, numeroita, pisteitä, viivoja ja alaviivoja.",
|
||||
"To continue, please enter your password.": "Ole hyvä ja syötä salasanasi jatkaaksesi.",
|
||||
"Verifies a user, device, and pubkey tuple": "Varmentaa käyttäjän, laitteen ja julkisen avaimen kolmikon",
|
||||
|
@ -580,8 +580,8 @@
|
|||
"Start chatting": "Aloita keskustelu",
|
||||
"Start Chatting": "Aloita keskustelu",
|
||||
"Click on the button below to start chatting!": "Paina nappia alla aloittaaksesi keskustelu!",
|
||||
"Username available": "Käyttäjänimi saatavissa",
|
||||
"Username not available": "Käyttäjänimi ei ole saatavissa",
|
||||
"Username available": "Käyttäjätunnus saatavilla",
|
||||
"Username not available": "Käyttäjätunnus ei ole saatavissa",
|
||||
"Something went wrong!": "Jokin meni vikaan!",
|
||||
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Tästä tulee tilisi nimi <span></span> -kotipalvelimella, tai voit valita <a>toisen palvelimen</a>.",
|
||||
"If you already have a Matrix account you can <a>log in</a> instead.": "Jos sinulla on jo Matrix-tili voit <a>kirjautua</a>.",
|
||||
|
@ -715,7 +715,7 @@
|
|||
"An email has been sent to %(emailAddress)s": "Sähköpostia lähetetty osoitteeseen %(emailAddress)s",
|
||||
"Please check your email to continue registration.": "Ole hyvä ja tarkista sähköpostisi jatkaaksesi.",
|
||||
"A text message has been sent to %(msisdn)s": "Tekstiviesti lähetetty numeroon %(msisdn)s",
|
||||
"Username on %(hs)s": "Käyttäjänimi palvelimella %(hs)s",
|
||||
"Username on %(hs)s": "Käyttäjätunnus palvelimella %(hs)s",
|
||||
"Custom server": "Muu palvelin",
|
||||
"Remove from community": "Poista yhteisöstä",
|
||||
"Disinvite this user from community?": "Peruuta tämän käyttäjän kutsu yhteisöön?",
|
||||
|
@ -736,8 +736,8 @@
|
|||
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
|
||||
"were unbanned %(count)s times|other": "porttikiellot poistettiin %(count)s kertaa",
|
||||
"And %(count)s more...|other": "Ja %(count)s lisää...",
|
||||
"Matrix ID": "Matrix ID",
|
||||
"Matrix Room ID": "Matrix huonetunniste",
|
||||
"Matrix ID": "Matrix-tunnus",
|
||||
"Matrix Room ID": "Matrix-huonetunnus",
|
||||
"email address": "sähköpostiosoite",
|
||||
"Try using one of the following valid address types: %(validTypesList)s.": "Kokeile käyttää yhtä näistä kelvollisista osoitetyypeistä: %(validTypesList)s.",
|
||||
"You have entered an invalid address.": "Olet syöttänyt virheellisen sähköpostiosoitteen.",
|
||||
|
@ -796,7 +796,7 @@
|
|||
"Please note you are logging into the %(hs)s server, not matrix.org.": "Huomaa että olet kirjautumassa palvelimelle %(hs)s, etkä palvelimelle matrix.org.",
|
||||
"Sign in to get started": "Kirjaudu aloittaksesi",
|
||||
"Upload an avatar:": "Lataa profiilikuva:",
|
||||
"Deops user with given id": "Poistaa annetun tunnisteen omaavalta käyttäjältä ylläpito-oikeudet",
|
||||
"Deops user with given id": "Poistaa tunnuksen mukaiselta käyttäjältä ylläpito-oikeudet",
|
||||
"Ignores a user, hiding their messages from you": "Jättää käyttäjän huomioimatta, jotta hänen viestejään ei näytetä sinulle",
|
||||
"Stops ignoring a user, showing their messages going forward": "Lopettaa käyttäjän huomiotta jättämisen, jotta hänen viestinsä näytetään sinulle",
|
||||
"Notify the whole room": "Ilmoita koko huoneelle",
|
||||
|
@ -810,8 +810,8 @@
|
|||
"Answer": "Vastaa",
|
||||
"Call Timeout": "Puhelun aikakatkaisu",
|
||||
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s %(day)s. %(monthName)s %(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s %(day)s. %(monthName)s %(fullYear)s %(time)s",
|
||||
"Ignored user": "Estetyt käyttäjät",
|
||||
"Unignored user": "Sallitut käyttäjät",
|
||||
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s tasolta %(fromPowerLevel)s tasolle %(toPowerLevel)s",
|
||||
|
@ -1345,11 +1345,11 @@
|
|||
"We encountered an error trying to restore your previous session.": "Törmäsimme ongelmaan yrittäessämme palauttaa edellistä istuntoasi.",
|
||||
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Jos olet aikaisemmin käyttänyt uudempaa versiota Riotista, istuntosi voi olla epäyhteensopiva tämän version kanssa. Sulje tämä ikkuna ja yritä uudemman version kanssa.",
|
||||
"The platform you're on": "Alusta, jolla olet",
|
||||
"Whether or not you're logged in (we don't record your username)": "Riippumatta siitä oletko kirjautunut sisään (emme tallenna käyttäjätunnustasi)",
|
||||
"Whether or not you're logged in (we don't record your username)": "Riippumatta siitä, oletko kirjautunut sisään (emme tallenna käyttäjätunnustasi)",
|
||||
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Riippumatta siitä, käytätkö muotoillun tekstin tilaa muotoilueditorissa",
|
||||
"Your User Agent": "Selaintunnisteesi",
|
||||
"The information being sent to us to help make Riot.im better includes:": "Tietoihin, jota lähetetään Riot.im:ään palvelun parantamiseksi, sisältyy:",
|
||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Niissä kohdissa, missä tämä sivu sisältää yksilöivää tietoa, kuten huoneen, käyttäjän tai ryhmän ID:n, kyseinen tieto poistetaan ennen tiedon lähetystä palvelimelle.",
|
||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kohdissa, joissa tämä sivu sisältää yksilöivää tietoa, kuten huoneen, käyttäjän tai ryhmän tunnuksen, kyseinen tieto poistetaan ennen palvelimelle lähettämistä.",
|
||||
"A conference call could not be started because the intgrations server is not available": "Konferenssipuhelua ei voitu aloittaa, koska integraatiopalvelin ei ole käytettävissä",
|
||||
"A call is currently being placed!": "Puhelua ollaan aloittamassa!",
|
||||
"A call is already in progress!": "Puhelu on jo meneillään!",
|
||||
|
@ -1406,7 +1406,7 @@
|
|||
"Enable Community Filter Panel": "Ota käyttöön yhteisön suodatinpaneeli",
|
||||
"Allow Peer-to-Peer for 1:1 calls": "Salli vertaisten väliset yhteydet kahdenkeskisissä puheluissa",
|
||||
"Prompt before sending invites to potentially invalid matrix IDs": "Kysy varmistus ennen kutsujen lähettämistä mahdollisesti epäkelpoihin Matrix ID:hin",
|
||||
"Messages containing my username": "Viestit, jotka sisältävät käyttäjänimeni",
|
||||
"Messages containing my username": "Viestit, jotka sisältävät käyttäjätunnukseni",
|
||||
"Messages containing @room": "Viestit, jotka sisältävät sanan ”@room”",
|
||||
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Turvalliset viestit tämän käyttäjän kanssa ovat salattuja päästä päähän, eivätkä kolmannet osapuolet voi lukea niitä.",
|
||||
"Thumbs up": "Peukut ylös",
|
||||
|
@ -1438,7 +1438,7 @@
|
|||
"For help with using Riot, click <a>here</a>.": "Saadaksesi apua Riotin käyttämisessä, klikkaa <a>tästä</a>.",
|
||||
"For help with using Riot, click <a>here</a> or start a chat with our bot using the button below.": "Saadaksesi apua Riotin käytössä, klikkaa <a>tästä</a> tai aloita keskustelu bottimme kanssa alla olevasta painikkeesta.",
|
||||
"Bug reporting": "Virheiden raportointi",
|
||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Jos olet ilmoittanut virheestä Githubin kautta, debug-lokit voivat auttaa meitä ongelman jäljittämisessä. Debug-lokit sisältävät ohjelman käyttödataa sisältäen käyttäjätunnuksen, vierailemiesi huoneiden tai ryhmien ID:t tai aliakset ja muiden käyttäjien käyttäjätunnukset. Debug-lokit eivät sisällä viestejä.",
|
||||
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Jos olet ilmoittanut virheestä Githubin kautta, debug-lokit voivat auttaa meitä ongelman jäljittämisessä. Debug-lokit sisältävät sovelluksen käyttödataa sisältäen käyttäjätunnuksen, vierailemiesi huoneiden tai ryhmien tunnukset tai aliakset ja muiden käyttäjien käyttäjätunnukset. Debug-lokit eivät sisällä viestejä.",
|
||||
"Autocomplete delay (ms)": "Automaattisen täydennyksen viive (ms)",
|
||||
"To link to this room, please add an alias.": "Lisää alias linkittääksesi tähän huoneeseen.",
|
||||
"Ignored users": "Hiljennetyt käyttäjät",
|
||||
|
@ -1465,7 +1465,7 @@
|
|||
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "Parhaan turvallisuuden takaamiseksi suosittelemme, että teet tämän kasvotusten tai muun luotetun viestintäkeinon avulla.",
|
||||
"Scissors": "Sakset",
|
||||
"Which officially provided instance you are using, if any": "Mitä virallisesti saatavilla olevaa instanssia käytät, jos mitään",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s %(day)s. %(monthName)s %(fullYear)s",
|
||||
"Missing roomId.": "roomId puuttuu.",
|
||||
"Forces the current outbound group session in an encrypted room to be discarded": "Pakottaa hylkäämään nykyisen ulospäin suuntautuvan ryhmäistunnon salatussa huoneessa",
|
||||
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s otti käyttöön tyylin ryhmille %(groups)s tässä huoneessa.",
|
||||
|
@ -1558,7 +1558,7 @@
|
|||
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Tapahtuman, johon oli vastattu, lataaminen epäonnistui. Se joko ei ole olemassa tai sinulla ei ole oikeutta katsoa sitä.",
|
||||
"The following users may not exist": "Seuraavat käyttäjät eivät välttämättä ole olemassa",
|
||||
"Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Alla luetelluille Matrix ID:ille ei löytynyt profiileja. Haluaisitko kutsua ne siitä huolimatta?",
|
||||
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug-lokit sisältävät ohjelman käyttödataa, kuten käyttäjätunnuksesi, huoneiden ja ryhmien ID:t tai aliakset, joissa olet vieraillut sekä muiden käyttäjien käyttäjätunnukset. Ne eivät sisällä viestejä.",
|
||||
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug-lokit sisältävät sovelluksen käyttödataa, kuten käyttäjätunnuksesi, vierailemiesi huoneiden ja ryhmien tunnukset tai aliakset, sekä muiden käyttäjien käyttäjätunnukset. Ne eivät sisällä viestejä.",
|
||||
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Ennen lokien lähettämistä sinun täytyy <a>luoda Githubiin issue (kysymys/ongelma)</a>, joka sisältää kuvauksen ongelmastasi.",
|
||||
"What GitHub issue are these logs for?": "Mihin Github-issueen nämä lokit liittyvät?",
|
||||
"Notes:": "Huomiot:",
|
||||
|
@ -1566,7 +1566,7 @@
|
|||
"Community IDs cannot be empty.": "Yhteisön ID:t eivät voi olla tyhjänä.",
|
||||
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Jotta et menetä keskusteluhistoriaasi, sinun täytyy tallentaa huoneen avaimet ennen kuin kirjaudut ulos. Joudut käyttämään uudempaa Riotin versiota tätä varten",
|
||||
"You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Olet aikaisemmin käyttänyt uudempaa Riotin versiota koneella %(host)s. Jotta voit käyttää tätä versiota osapuolten välisellä salauksella, sinun täytyy kirjautua ulos ja kirjautua takaisin sisään. ",
|
||||
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Tämä tekee tilistäsi lopullisesti käyttökelvottoman. Et voi kirjautua sisään, eikä kukaan voi rekisteröidä tunnusta samalla käyttäjä-ID:llä. Tunnuksesi poistuu kaikista huoneista, joihin se on liittynyt, ja tilisi tiedot poistetaan identiteettipalvelimelta. <b>Tämä toimenpide on lopullinen eikä sitä voi kumota.</b>",
|
||||
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Tämä tekee tilistäsi lopullisesti käyttökelvottoman. Et voi kirjautua sisään, eikä kukaan voi rekisteröidä samaa käyttäjätunnusta. Tilisi poistuu kaikista huoneista, joihin se on liittynyt, ja tilisi tiedot poistetaan identiteettipalvelimeltasi. <b>Tämä toimenpidettä ei voi kumota.</b>",
|
||||
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Tilisi poistaminen käytöstä <b>ei oletuksena saa meitä unohtamaan lähettämiäsi viestejä.</b> Jos haluaisit meidän unohtavan viestisi, rastita alla oleva ruutu.",
|
||||
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Viestien näkyvyys Matrixissa on samantapainen kuin sähköpostissa. Vaikka se, että unohdamme viestisi, tarkoittaa, ettei viestejäsi jaeta enää uusille tai rekisteröitymättömille käyttäjille, käyttäjät, jotka ovat jo saaneet viestisi pystyvät lukemaan jatkossakin omaa kopiotaan viesteistäsi.",
|
||||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Unohda kaikki viestit, jotka olen lähettänyt, kun tilini on poistettu käytöstä (b>Varoitus:</b> tästä seuraa, että tulevat käyttäjät näkevät epätäydellisen version keskusteluista)",
|
||||
|
@ -1627,7 +1627,7 @@
|
|||
"Use an email address to recover your account. Other users can invite you to rooms using your contact details.": "Käytä sähköpostia tilisi palauttamiseen. Muut käyttäjät voivat kutsua sinut huoneisiin yhteystiedoillasi.",
|
||||
"Other servers": "Muut palvelimet",
|
||||
"Enter custom server URLs <a>What does this mean?</a>": "Syötä mukautettujen palvelinten osoitteet. <a>Mitä tämä tarkoittaa?</a>",
|
||||
"Free": "Vapaa",
|
||||
"Free": "Ilmainen",
|
||||
"Join millions for free on the largest public server": "Liity ilmaiseksi miljoonien joukkoon suurimmalla julkisella palvelimella",
|
||||
"Premium": "Premium",
|
||||
"Premium hosting for organisations <a>Learn more</a>": "Premium-ylläpitoa organisaatioille. <a>Lue lisää</a>",
|
||||
|
@ -1713,7 +1713,7 @@
|
|||
"Send debug logs and reload Riot": "Lähetä debug-lokit ja päivitä Riot",
|
||||
"Reload Riot without sending logs": "Päivitä Riot lähettämättä lokeja",
|
||||
"A widget would like to verify your identity": "Sovelma haluaisi vahvistaa identiteettisi",
|
||||
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Sovelma osoitteessa %(widgetUrl)s haluaisi vahvistaa identiteettisi. Jos sallit tämän, sovelma pystyy vahvistamaan käyttäjä-ID:si, mutta ei voi toimia nimelläsi.",
|
||||
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Sovelma osoitteessa %(widgetUrl)s haluaisi todentaa henkilöllisyytesi. Jos sallit tämän, sovelma pystyy todentamaan käyttäjätunnuksesi, muttei voi toimia nimissäsi.",
|
||||
"Remember my selection for this widget": "Muista valintani tälle sovelmalle",
|
||||
"Deny": "Kiellä",
|
||||
"Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Tunnistimme dataa, joka on lähtöisin vanhasta Riotin versiosta. Tämä aiheuttaa toimintahäiriöitä osapuolten välisessä salauksessa vanhassa versiossa. Viestejä, jotka ovat salattu osapuolten välisellä salauksella, ei välttämättä voida purkaa tällä versiolla. Tämä voi myös aiheuttaa epäonnistumisia viestien välityksessä tämän version kanssa. Jos kohtaat ongelmia, kirjaudu ulos ja takaisin sisään. Säilyttääksesi viestihistoriasi, tallenna salausavaimesi ja tuo ne takaisin kirjauduttuasi takaisin sisälle.",
|
||||
|
@ -1830,12 +1830,25 @@
|
|||
"Enter phone number (required on this homeserver)": "Syötä puhelinnumero (vaaditaan tällä kotipalvelimella)",
|
||||
"Doesn't look like a valid phone number": "Ei näytä kelvolliselta puhelinnumerolta",
|
||||
"Use letters, numbers, dashes and underscores only": "Käytä vain kirjaimia, numeroita, viivoja ja alaviivoja",
|
||||
"Enter username": "Syötä käyttäjänimi",
|
||||
"Enter username": "Syötä käyttäjätunnus",
|
||||
"Some characters not allowed": "Osaa merkeistä ei sallita",
|
||||
"Use an email address to recover your account.": "Palauta tilisi sähköpostiosoitteen avulla.",
|
||||
"Other users can invite you to rooms using your contact details.": "Muut käyttäjät voivat kutsua sinut huoneisiin yhteystietojesi avulla.",
|
||||
"Error loading Riot": "Virhe Riotin lataamisessa",
|
||||
"If this is unexpected, please contact your system administrator or technical support representative.": "Jos et odottanut tätä, ota yhteyttä järjestelmänvalvojaan tai tekniseen tukeen.",
|
||||
"Homeserver URL does not appear to be a valid Matrix homeserver": "Kotipalvelimen osoite ei näytä olevan kelvollinen Matrix-kotipalvelin",
|
||||
"Identity server URL does not appear to be a valid identity server": "Identiteettipalvelimen osoite ei näytä olevan kelvollinen identiteettipalvelin"
|
||||
"Identity server URL does not appear to be a valid identity server": "Identiteettipalvelimen osoite ei näytä olevan kelvollinen identiteettipalvelin",
|
||||
"A conference call could not be started because the integrations server is not available": "Konferenssipuhelua ei voitu aloittaa, koska integraatiopalvelin ei ole käytettävissä",
|
||||
"When rooms are upgraded": "Kun huoneet päivitetään",
|
||||
"Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Yhdistä tämä laite avainten varmuuskopiointiin ennen kuin kirjaudut ulos, jotta et menetä mahdollisia vain tällä laitteella olevia avaimia.",
|
||||
"Rejecting invite …": "Hylätään kutsua …",
|
||||
"You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s poisti sinut huoneesta %(roomName)s",
|
||||
"Edited at %(date)s.": "Muokattu %(date)s.",
|
||||
"edited": "muokattu",
|
||||
"To help us prevent this in future, please <a>send us logs</a>.": "Voit auttaa meitä estämään tämän toistumisen <a>lähettämällä meille lokeja</a>.",
|
||||
"Name or Matrix ID": "Nimi tai Matrix-tunnus",
|
||||
"Email, name or Matrix ID": "Sähköposti, nimi tai Matrix-tunnus",
|
||||
"Edited at %(date)s": "Muokattu %(date)s",
|
||||
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Tiedosto on <b>liian iso</b> ladattavaksi. Tiedostojen kokoraja on %(limit)s mutta tämä tiedosto on %(sizeOfThisFile)s.",
|
||||
"Unbans user with given ID": "Poistaa porttikiellon tunnuksen mukaiselta käyttäjältä"
|
||||
}
|
||||
|
|
|
@ -198,7 +198,7 @@
|
|||
"No devices with registered encryption keys": "Pas d’appareil avec des clés de chiffrement enregistrées",
|
||||
"No more results": "Fin des résultats",
|
||||
"No results": "Pas de résultat",
|
||||
"unknown error code": "Code d'erreur inconnu",
|
||||
"unknown error code": "code d’erreur inconnu",
|
||||
"OK": "OK",
|
||||
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Une fois le chiffrement activé dans un salon il ne peut pas être désactivé (pour le moment)",
|
||||
"Only people who have been invited": "Seules les personnes ayant été invitées",
|
||||
|
@ -313,7 +313,7 @@
|
|||
"Unknown room %(roomId)s": "Salon inconnu %(roomId)s",
|
||||
"Unmute": "Activer le son",
|
||||
"Upload avatar": "Télécharger une photo de profil",
|
||||
"Upload Failed": "Erreur lors de l'envoi",
|
||||
"Upload Failed": "Échec de l’envoi",
|
||||
"Upload Files": "Télécharger les fichiers",
|
||||
"Upload file": "Envoyer un fichier",
|
||||
"Usage": "Utilisation",
|
||||
|
@ -422,9 +422,9 @@
|
|||
"You must join the room to see its files": "Vous devez rejoindre le salon pour voir ses fichiers",
|
||||
"Reject all %(invitedRooms)s invites": "Rejeter la totalité des %(invitedRooms)s invitations",
|
||||
"Start new chat": "Démarrer une nouvelle discussion",
|
||||
"Failed to invite": "Echec de l'invitation",
|
||||
"Failed to invite user": "Echec lors de l'invitation de l'utilisateur",
|
||||
"Failed to invite the following users to the %(roomName)s room:": "Echec lors de l’invitation des utilisateurs suivants dans le salon %(roomName)s :",
|
||||
"Failed to invite": "Échec de l’invitation",
|
||||
"Failed to invite user": "Échec lors de l'invitation de l'utilisateur",
|
||||
"Failed to invite the following users to the %(roomName)s room:": "Échec de l’invitation des utilisateurs suivants dans le salon %(roomName)s :",
|
||||
"Confirm Removal": "Confirmer la suppression",
|
||||
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Voulez-vous vraiment supprimer cet événement ? Notez que si vous supprimez le changement du nom ou du sujet d’un salon, il est possible que ce changement soit annulé.",
|
||||
"Unknown error": "Erreur inconnue",
|
||||
|
@ -641,7 +641,7 @@
|
|||
"Revoke widget access": "Révoquer les accès du widget",
|
||||
"Sets the room topic": "Défini le sujet du salon",
|
||||
"To get started, please pick a username!": "Pour commencer, choisissez un nom d'utilisateur !",
|
||||
"Unable to create widget.": "Impossible de créer un widget.",
|
||||
"Unable to create widget.": "Impossible de créer le widget.",
|
||||
"Unbans user with given id": "Révoque le bannissement de l'utilisateur à partir de son identifiant",
|
||||
"You are not in this room.": "Vous n'êtes pas dans ce salon.",
|
||||
"You do not have permission to do that in this room.": "Vous n'avez pas la permission d'effectuer cette action dans ce salon.",
|
||||
|
@ -716,7 +716,7 @@
|
|||
"%(names)s and %(count)s others are typing|other": "%(names)s et %(count)s autres écrivent",
|
||||
"Jump to read receipt": "Aller à l'accusé de lecture",
|
||||
"World readable": "Lisible publiquement",
|
||||
"Guests can join": "Les invités peuvent rejoindre le salon",
|
||||
"Guests can join": "Accessible aux invités",
|
||||
"To invite users into the room, you must be a": "Pour inviter des utilisateurs dans le salon, vous devez être un(e)",
|
||||
"To configure the room, you must be a": "Pour configurer le salon, vous devez être un(e)",
|
||||
"To kick users, you must be a": "Pour expulser des utilisateurs, vous devez être",
|
||||
|
@ -1019,7 +1019,7 @@
|
|||
"Cancel Sending": "Annuler l'envoi",
|
||||
"This Room": "Ce salon",
|
||||
"The Home Server may be too old to support third party networks": "Le serveur d'accueil semble trop ancien pour supporter des réseaux tiers",
|
||||
"Noisy": "Bruyant",
|
||||
"Noisy": "Sonore",
|
||||
"Room not found": "Salon non trouvé",
|
||||
"Messages containing my display name": "Messages contenant mon nom affiché",
|
||||
"Messages in one-to-one chats": "Messages dans les discussions directes",
|
||||
|
@ -1892,5 +1892,86 @@
|
|||
"Upload %(count)s other files|other": "Envoyer %(count)s autres fichiers",
|
||||
"Upload %(count)s other files|one": "Envoyer %(count)s autre fichier",
|
||||
"Cancel All": "Tout annuler",
|
||||
"Upload Error": "Erreur d’envoi"
|
||||
"Upload Error": "Erreur d’envoi",
|
||||
"A conference call could not be started because the integrations server is not available": "L’appel en téléconférence n’a pas pu être lancé car les intégrations du serveur ne sont pas disponibles",
|
||||
"The server does not support the room version specified.": "Le serveur ne prend pas en charge la version de salon spécifiée.",
|
||||
"Name or Matrix ID": "Nom ou identifiant Matrix",
|
||||
"Email, name or Matrix ID": "E-mail, nom ou identifiant Matrix",
|
||||
"Please confirm that you'd like to go forward with upgrading this room from <oldVersion /> to <newVersion />.": "Veuillez confirmer la mise à niveau de ce salon de <oldVersion /> à <newVersion />.",
|
||||
"Changes your avatar in this current room only": "Change votre avatar seulement dans le salon actuel",
|
||||
"Unbans user with given ID": "Révoque le bannissement de l’utilisateur ayant l’identifiant fourni",
|
||||
"Sends the given message coloured as a rainbow": "Envoie le message coloré aux couleurs de l’arc-en-ciel",
|
||||
"Sends the given emote coloured as a rainbow": "Envoie l’émoji coloré aux couleurs de l’arc-en-ciel",
|
||||
"The user's homeserver does not support the version of the room.": "Le serveur d’accueil de l’utilisateur ne prend pas en charge la version de ce salon.",
|
||||
"Edit messages after they have been sent (refresh to apply changes)": "Éditer les messages après leur envoi (actualiser pour appliquer les changements)",
|
||||
"React to messages with emoji (refresh to apply changes)": "Réagir aux messages avec des émojis (actualiser pour appliquer les changements)",
|
||||
"When rooms are upgraded": "Quand les salons sont mis à niveau",
|
||||
"This device is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "Cet appareil <b>ne sauvegarde pas vos clés</b>, mais vous avez une sauvegarde existante que vous pouvez restaurer et joindre.",
|
||||
"Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Connecter cet appareil à la sauvegarde de clés avant de vous déconnecter pour éviter de perdre des clés qui pourraient n’être présentes que sur cet appareil.",
|
||||
"Connect this device to Key Backup": "Connecter cet appareil à la sauvegarde de clés",
|
||||
"Backup has an <validity>invalid</validity> signature from this device": "La sauvegarde a une signature <validity>invalide</validity> depuis cet appareil",
|
||||
"this room": "ce salon",
|
||||
"View older messages in %(roomName)s.": "Voir les messages plus anciens dans %(roomName)s.",
|
||||
"Joining room …": "Inscription au salon…",
|
||||
"Loading …": "Chargement…",
|
||||
"Rejecting invite …": "Rejet de l’invitation…",
|
||||
"Join the conversation with an account": "Rejoindre la conversation avec un compte",
|
||||
"Sign Up": "S’inscrire",
|
||||
"Sign In": "Se connecter",
|
||||
"You were kicked from %(roomName)s by %(memberName)s": "Vous avez été expulsé(e) de %(roomName)s par %(memberName)s",
|
||||
"Reason: %(reason)s": "Motif : %(reason)s",
|
||||
"Forget this room": "Oublier ce salon",
|
||||
"Re-join": "Revenir",
|
||||
"You were banned from %(roomName)s by %(memberName)s": "Vous avez été banni(e) de %(roomName)s par %(memberName)s",
|
||||
"Something went wrong with your invite to %(roomName)s": "Une erreur est survenue avec votre invitation à %(roomName)s",
|
||||
"%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "%(errcode)s a été retourné en essayant de valider votre invitation. Vous pouvez essayer de transmettre cette information à l’administrateur du salon.",
|
||||
"You can only join it with a working invite.": "Vous ne pouvez le rejoindre qu’avec une invitation fonctionnelle.",
|
||||
"You can still join it because this is a public room.": "Vous pouvez quand même le rejoindre car c’est un salon public.",
|
||||
"Join the discussion": "Rejoindre la discussion",
|
||||
"Try to join anyway": "Essayer de le rejoindre quand même",
|
||||
"This invite to %(roomName)s wasn't sent to your account": "Cette invitation à %(roomName)s n’a pas été envoyée à votre compte",
|
||||
"Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Connectez-vous avec un autre compte, demandez une autre invitation ou ajoutez l’adresse e-mail %(email)s à ce compte.",
|
||||
"Do you want to chat with %(user)s?": "Voulez-vous discuter avec %(user)s ?",
|
||||
"Do you want to join %(roomName)s?": "Voulez-vous rejoindre %(roomName)s ?",
|
||||
"<userName/> invited you": "<userName/> vous a invité(e)",
|
||||
"You're previewing %(roomName)s. Want to join it?": "Ceci est un aperçu de %(roomName)s. Voulez-vous le rejoindre ?",
|
||||
"%(roomName)s can't be previewed. Do you want to join it?": "Vous ne pouvez pas avoir d’aperçu de %(roomName)s. Voulez-vous le rejoindre ?",
|
||||
"This room doesn't exist. Are you sure you're at the right place?": "Ce salon n’existe pas. Êtes-vous vraiment au bon endroit ?",
|
||||
"Try again later, or ask a room admin to check if you have access.": "Réessayez plus tard ou demandez à l’administrateur du salon si vous y avez accès.",
|
||||
"%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s a été retourné en essayant d’accéder au salon. Si vous pensez que vous ne devriez pas voir ce message, veuillez <issueLink>soumettre un rapport d’anomalie</issueLink>.",
|
||||
"This room has already been upgraded.": "Ce salon a déjà été mis à niveau.",
|
||||
"Agree or Disagree": "Accepter ou refuser",
|
||||
"Like or Dislike": "Aimer ou ne pas aimer",
|
||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>ont réagi avec %(shortName)s</reactedWith>",
|
||||
"Edited at %(date)s.": "Édité à %(date)s.",
|
||||
"edited": "édité",
|
||||
"Rotate Left": "Tourner à gauche",
|
||||
"Rotate Right": "Tourner à droite",
|
||||
"View Servers in Room": "Voir les serveurs dans le salon",
|
||||
"Use an email address to recover your account": "Utiliser une adresse e-mail pour récupérer votre compte",
|
||||
"Enter email address (required on this homeserver)": "Saisir l’adresse e-mail (obligatoire sur ce serveur d’accueil)",
|
||||
"Doesn't look like a valid email address": "Cela ne ressemble pas a une adresse e-mail valide",
|
||||
"Enter password": "Saisir le mot de passe",
|
||||
"Password is allowed, but unsafe": "Ce mot de passe est autorisé, mais peu sûr",
|
||||
"Nice, strong password!": "Bien joué, un mot de passe robuste !",
|
||||
"Passwords don't match": "Les mots de passe ne correspondent pas",
|
||||
"Other users can invite you to rooms using your contact details": "D’autres utilisateurs peuvent vous inviter à des salons grâce à vos informations de contact",
|
||||
"Enter phone number (required on this homeserver)": "Saisir le numéro de téléphone (obligatoire sur ce serveur d’accueil)",
|
||||
"Doesn't look like a valid phone number": "Cela ne ressemble pas à un numéro de téléphone valide",
|
||||
"Use letters, numbers, dashes and underscores only": "Utilisez uniquement des lettres, chiffres, traits d’union et tirets bas",
|
||||
"Enter username": "Saisir le nom d’utilisateur",
|
||||
"Some characters not allowed": "Certains caractères ne sont pas autorisés",
|
||||
"Use an email address to recover your account.": "Utilisez une adresse e-mail pour récupérer votre compte.",
|
||||
"Other users can invite you to rooms using your contact details.": "D’autre utilisateurs peuvent vous inviter à des salons en utilisant vos informations de contact.",
|
||||
"Error loading Riot": "Erreur lors du chargement de Riot",
|
||||
"If this is unexpected, please contact your system administrator or technical support representative.": "Si cela est inattendu, veuillez contacter votre administrateur système ou un représentant du support technique.",
|
||||
"Failed to get autodiscovery configuration from server": "Échec de la découverte automatique de la configuration depuis le serveur",
|
||||
"Invalid base_url for m.homeserver": "base_url pour m.homeserver non valide",
|
||||
"Homeserver URL does not appear to be a valid Matrix homeserver": "L’URL du serveur d’accueil ne semble pas être un serveur d’accueil Matrix valide",
|
||||
"Invalid base_url for m.identity_server": "base_url pour m.identity_server non valide",
|
||||
"Identity server URL does not appear to be a valid identity server": "L’URL du serveur d’identité ne semble pas être un serveur d’identité valide",
|
||||
"Edited at %(date)s": "Édité à %(date)s",
|
||||
"Show hidden events in timeline": "Afficher les évènements cachés dans l’historique",
|
||||
"Your profile": "Votre profil",
|
||||
"Add room": "Ajouter un salon"
|
||||
}
|
||||
|
|
|
@ -1962,5 +1962,16 @@
|
|||
"Invalid base_url for m.homeserver": "Hibás base_url az m.homeserver -hez",
|
||||
"Homeserver URL does not appear to be a valid Matrix homeserver": "A matrix URL nem tűnik érvényesnek",
|
||||
"Invalid base_url for m.identity_server": "Érvénytelen base_url az m.identity_server -hez",
|
||||
"Identity server URL does not appear to be a valid identity server": "Az Azonosító szerver URL nem tűnik érvényesnek"
|
||||
"Identity server URL does not appear to be a valid identity server": "Az Azonosító szerver URL nem tűnik érvényesnek",
|
||||
"A conference call could not be started because the integrations server is not available": "A konferencia hívást nem lehet elkezdeni mert az integrációs szerver nem érhető el",
|
||||
"Name or Matrix ID": "Név vagy Matrix azon.",
|
||||
"Email, name or Matrix ID": "E-mail, név vagy Matrix azon.",
|
||||
"Unbans user with given ID": "Visszaengedi a megadott azonosítójú felhasználót",
|
||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>ezzel reagált: %(shortName)s</reactedWith>",
|
||||
"Edited at %(date)s.": "Szerkesztve ekkor: %(date)s.",
|
||||
"edited": "szerkesztve",
|
||||
"Edited at %(date)s": "Szerkesztve: %(date)s",
|
||||
"Show hidden events in timeline": "Rejtett események megmutatása az idővonalon",
|
||||
"Add room": "Szoba hozzáadása",
|
||||
"Your profile": "Profilod"
|
||||
}
|
||||
|
|
|
@ -1126,5 +1126,10 @@
|
|||
"Collapse panel": "Sakļaut (saritināt) paneli",
|
||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Tavā pašreizējā pārlūkā aplikācijas izskats un uzvedība var būt pilnīgi neatbilstoša, kā arī dažas no visām funkcijām var nedarboties. Ja vēlies turpināt izmantot šo pārlūku, Tu vari arī turpināt, apzinoties, ka šajā gadījumā esi viens/a ar iespējamo problēmu!",
|
||||
"Checking for an update...": "Lūkojos pēc aktualizācijas...",
|
||||
"There are advanced notifications which are not shown here": "Pastāv papildus paziņojumi, kuri šeit netiek rādīti"
|
||||
"There are advanced notifications which are not shown here": "Pastāv papildus paziņojumi, kuri šeit netiek rādīti",
|
||||
"e.g. %(exampleValue)s": "piemēram %(exampleValue)s",
|
||||
"e.g. <CurrentPageURL>": "piemēram <CurrentPageURL>",
|
||||
"Your device resolution": "Tavas iekārtas izšķirtspēja",
|
||||
"Sign In": "Ienākt",
|
||||
"You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Varat arī iestatīt pielāgotu identitātes serveri, bet jūs nevarēsiet uzaicināt lietotājus izmantojot e-pasta adresi, kā arī tikt uzaicināts pēc e-pasta adreses."
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"Are you sure you want to reject the invitation?": "Czy na pewno chcesz odrzucić zaproszenie?",
|
||||
"Are you sure you want to upload the following files?": "Czy na pewno chcesz przesłać następujące pliki?",
|
||||
"Autoplay GIFs and videos": "Automatycznie odtwarzaj GIFy i filmiki",
|
||||
"%(senderName)s banned %(targetName)s.": "%(senderName)s zbanował %(targetName)s.",
|
||||
"%(senderName)s banned %(targetName)s.": "%(senderName)s zbanował(a) %(targetName)s.",
|
||||
"Ban": "Zbanuj",
|
||||
"Bans user with given id": "Blokuje użytkownika o podanym ID",
|
||||
"Blacklisted": "Umieszczono na czarnej liście",
|
||||
|
@ -456,7 +456,7 @@
|
|||
"Unable to create widget.": "Nie można utworzyć widżetu.",
|
||||
"Unable to remove contact information": "Nie można usunąć informacji kontaktowych",
|
||||
"Unable to verify email address.": "Weryfikacja adresu e-mail nie powiodła się.",
|
||||
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s odblokował/a %(targetName)s.",
|
||||
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s odblokował(a) %(targetName)s.",
|
||||
"Unable to capture screen": "Nie można zrobić zrzutu ekranu",
|
||||
"Unable to enable Notifications": "Nie można włączyć powiadomień",
|
||||
"Unable to load device list": "Nie można załadować listy urządzeń",
|
||||
|
@ -1337,5 +1337,103 @@
|
|||
"Show read receipts": "Wyświetl potwierdzenia odczytu",
|
||||
"Send typing notifications": "Wyślij powiadomienia o pisaniu",
|
||||
"I don't want my encrypted messages": "Nie chcę moich zaszyfrowanych wiadomości",
|
||||
"You'll lose access to your encrypted messages": "Utracisz dostęp do zaszyfrowanych wiadomości"
|
||||
"You'll lose access to your encrypted messages": "Utracisz dostęp do zaszyfrowanych wiadomości",
|
||||
"Verified!": "Zweryfikowano!",
|
||||
"Dog": "Pies",
|
||||
"Cat": "Kot",
|
||||
"Lion": "Lew",
|
||||
"Horse": "Koń",
|
||||
"Unicorn": "Jednorożec",
|
||||
"Pig": "Świnia",
|
||||
"Elephant": "Słoń",
|
||||
"Rabbit": "Królik",
|
||||
"Panda": "Panda",
|
||||
"Rooster": "Kogut",
|
||||
"Penguin": "Pingwin",
|
||||
"Turtle": "Żółw",
|
||||
"Fish": "Ryba",
|
||||
"Octopus": "Ośmiornica",
|
||||
"Butterfly": "Motyl",
|
||||
"Flower": "Kwiat",
|
||||
"Tree": "Drzewo",
|
||||
"Cactus": "Kaktus",
|
||||
"Mushroom": "Grzyb",
|
||||
"Moon": "Księżyc",
|
||||
"Cloud": "Chmura",
|
||||
"Fire": "Ogień",
|
||||
"Banana": "Banan",
|
||||
"Apple": "Jabłko",
|
||||
"Strawberry": "Truskawka",
|
||||
"Corn": "Kukurydza",
|
||||
"Pizza": "Pizza",
|
||||
"Cake": "Ciasto",
|
||||
"Heart": "Serce",
|
||||
"Robot": "Robot",
|
||||
"Hat": "Kapelusz",
|
||||
"Glasses": "Okulary",
|
||||
"Umbrella": "Parasol",
|
||||
"Hourglass": "Klepsydra",
|
||||
"Clock": "Zegar",
|
||||
"Light bulb": "Żarówka",
|
||||
"Book": "Książka",
|
||||
"Pencil": "Ołówek",
|
||||
"Paperclip": "Spinacz",
|
||||
"Scissors": "Nożyczki",
|
||||
"Padlock": "Kłódka",
|
||||
"Key": "Klucz",
|
||||
"Telephone": "Telefon",
|
||||
"Flag": "Flaga",
|
||||
"Train": "Pociąg",
|
||||
"Bicycle": "Rower",
|
||||
"Aeroplane": "Samolot",
|
||||
"Rocket": "Rakieta",
|
||||
"Trophy": "Trofeum",
|
||||
"Guitar": "Gitara",
|
||||
"Trumpet": "Trąbka",
|
||||
"Bell": "Dzwonek",
|
||||
"Anchor": "Kotwica",
|
||||
"Headphones": "Słuchawki",
|
||||
"Folder": "Folder",
|
||||
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "W celu zapewnienia maksymalnego bezpieczeństwa zalecamy, abyś zrobił to osobiście lub skorzystał z innego zaufanego środka komunikacji.",
|
||||
"Phone Number": "Numer telefonu",
|
||||
"Display Name": "Wyświetlana nazwa",
|
||||
"Set a new account password...": "Ustaw nowe hasło do konta…",
|
||||
"Email addresses": "Adresy E-mail",
|
||||
"Phone numbers": "Numery telefonów",
|
||||
"Language and region": "Język i region",
|
||||
"Theme": "Motyw",
|
||||
"Account management": "Zarządzanie kontem",
|
||||
"Bug reporting": "Zgłaszanie błędów",
|
||||
"Versions": "Wersje",
|
||||
"Preferences": "Preferencje",
|
||||
"Timeline": "Oś czasu",
|
||||
"Room list": "Lista pokoi",
|
||||
"Security & Privacy": "Bezpieczeństwo i prywatność",
|
||||
"Room Addresses": "Adresy pokoju",
|
||||
"Change room avatar": "Zmień awatar pokoju",
|
||||
"Change room name": "Zmień nazwę pokoju",
|
||||
"Change permissions": "Zmieniać uprawnienia",
|
||||
"Change topic": "Zmieniać temat",
|
||||
"Default role": "Domyślna rola",
|
||||
"Send messages": "Wysyłanie wiadomości",
|
||||
"Change settings": "Zmieniać ustawienia",
|
||||
"Remove messages": "Usuwanie wiadomości",
|
||||
"Notify everyone": "Powiadamianie wszystkich",
|
||||
"Roles & Permissions": "Role i uprawnienia",
|
||||
"Encryption": "Szyfrowanie",
|
||||
"Join the conversation with an account": "Przyłącz się do rozmowy przy użyciu konta",
|
||||
"Sign Up": "Zarejestruj się",
|
||||
"Join the discussion": "Dołącz do dyskusji",
|
||||
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s nie może być wyświetlony. Chcesz do niego dołączyć?",
|
||||
"Main address": "Główny adres",
|
||||
"Room avatar": "Awatar pokoju",
|
||||
"Upload room avatar": "Prześlij awatar pokoju",
|
||||
"Room Name": "Nazwa pokoju",
|
||||
"Room Topic": "Temat pokoju",
|
||||
"Power level": "Poziom uprawnień",
|
||||
"Verify by comparing a short text string.": "Weryfikuj, porównując krótki ciąg tekstu.",
|
||||
"Begin Verifying": "Rozpocznij weryfikację",
|
||||
"Waiting for partner to accept...": "Czekanie, aż partner zaakceptuje…",
|
||||
"Room Settings - %(roomName)s": "Ustawienia pokoju - %(roomName)s",
|
||||
"Doesn't look like a valid phone number": "To nie wygląda na poprawny numer telefonu"
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@
|
|||
"Sign In": "Prijava",
|
||||
"powered by Matrix": "poganja Matrix",
|
||||
"Custom Server Options": "Možnosti strežnika po meri",
|
||||
"You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Nastavite lahko tudi strežnik za identiteto po meri, vendar ne boste mogli povabiti uporabnikov prek e-pošte, prav tako pa vas ne bodo mogli povabiti drugi."
|
||||
"You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Nastavite lahko tudi strežnik za identiteto po meri, vendar ne boste mogli povabiti uporabnikov prek e-pošte, prav tako pa vas ne bodo mogli povabiti drugi.",
|
||||
"Your language of choice": "Vaš jezik po izbiri"
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Automatically focuses the captured reference when receiving a non-null
|
||||
* object. Useful in scenarios where componentDidMount does not have a
|
||||
* useful reference to an element, but one needs to focus the element on
|
||||
* first render. Example usage: ref={focusCapturedRef}
|
||||
* @param {function} ref The React reference to focus on, if not null
|
||||
*/
|
||||
export function focusCapturedRef(ref) {
|
||||
if (ref) {
|
||||
ref.focus();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Based on...
|
||||
* ChromaCheck 1.16
|
||||
* author Roel Nieskens, https://pixelambacht.nl
|
||||
* MIT license
|
||||
*/
|
||||
|
||||
let colrFontSupported = undefined;
|
||||
|
||||
async function isColrFontSupported() {
|
||||
if (colrFontSupported !== undefined) {
|
||||
return colrFontSupported;
|
||||
}
|
||||
|
||||
// Firefox has supported COLR fonts since version 26
|
||||
// but doesn't support the check below with content blocking enabled.
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
colrFontSupported = true;
|
||||
return colrFontSupported;
|
||||
}
|
||||
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
const img = new Image();
|
||||
// eslint-disable-next-line
|
||||
const fontCOLR = 'd09GRgABAAAAAAKAAAwAAAAAAowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDT0xSAAACVAAAABYAAAAYAAIAJUNQQUwAAAJsAAAAEgAAABLJAAAQT1MvMgAAAYAAAAA6AAAAYBfxJ0pjbWFwAAABxAAAACcAAAAsAAzpM2dseWYAAAH0AAAAGgAAABoNIh0kaGVhZAAAARwAAAAvAAAANgxLumdoaGVhAAABTAAAABUAAAAkCAEEAmhtdHgAAAG8AAAABgAAAAYEAAAAbG9jYQAAAewAAAAGAAAABgANAABtYXhwAAABZAAAABsAAAAgAg4AHW5hbWUAAAIQAAAAOAAAAD4C5wsecG9zdAAAAkgAAAAMAAAAIAADAAB4AWNgZGAAYQ5+qdB4fpuvDNIsDCBwaQGTAIi+VlscBaJZGMDiHAxMIAoAtjIF/QB4AWNgZGBgYQACOAkUQQWMAAGRABAAAAB4AWNgZGBgYGJgAdMMUJILJMQgAWICAAH3AC4AeAFjYGFhYJzAwMrAwDST6QwDA0M/hGZ8zWDMyMmAChgFkDgKQMBw4CXDSwYWEBdIYgAFBgYA/8sIdAAABAAAAAAAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoRA/kuG//8hpDgjWJ4BAFVMBiYAAAAAAAANAAAAAQAAAAAEAAQAAAMAABEhESEEAPwABAD8AAAAeAEtxgUNgAAAAMHHIQTShTlOAty9/4bf7AARCwlBNhBw4L/43qXjYGUmf19TMuLcj/BJL3XfBg54AWNgZsALAAB9AAR4AWNgYGAEYj4gFgGygGwICQACOwAoAAAAAAABAAEAAQAAAA4AAAAAyP8AAA==';
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="100" style="background:#fff;fill:#000;">
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: "chromacheck-colr";
|
||||
src: url(data:application/x-font-woff;base64,${fontCOLR}) format("woff");
|
||||
}
|
||||
</style>
|
||||
<text x="0" y="0" font-size="20">
|
||||
<tspan font-family="chromacheck-colr" x="0" dy="20"></tspan>
|
||||
</text>
|
||||
</svg>`;
|
||||
canvas.width = 20;
|
||||
canvas.height = 100;
|
||||
|
||||
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
|
||||
|
||||
// FIXME wait for safari load our colr font
|
||||
const wait = ms => new Promise((r, j)=>setTimeout(r, ms));
|
||||
await wait(500);
|
||||
|
||||
context.drawImage(img, 0, 0);
|
||||
colrFontSupported = (context.getImageData(10, 10, 1, 1).data[0] === 200);
|
||||
} catch (e) {
|
||||
console.error("Couldn't load colr font", e);
|
||||
colrFontSupported = false;
|
||||
}
|
||||
return colrFontSupported;
|
||||
}
|
||||
|
||||
export async function fixupColorFonts() {
|
||||
if (colrFontSupported !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await isColrFontSupported()) {
|
||||
const path = `url('${require("../../res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2")}')`;
|
||||
document.fonts.add(new FontFace("Twemoji", path, {}));
|
||||
// For at least Chrome on Windows 10, we have to explictly add extra
|
||||
// weights for the emoji to appear in bold messages, etc.
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: 600 }));
|
||||
document.fonts.add(new FontFace("Twemoji", path, { weight: 700 }));
|
||||
}
|
||||
// if not supported, the browser will fall back to one of the native fonts specified.
|
||||
}
|
||||
|
13
yarn.lock
13
yarn.lock
|
@ -2576,10 +2576,15 @@ emoji-regex@^7.0.1:
|
|||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
|
||||
|
||||
emojione@2.2.7:
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/emojione/-/emojione-2.2.7.tgz#46457cf6b9b2f8da13ae8a2e4e547de06ee15e96"
|
||||
integrity sha1-RkV89rmy+NoTroouTlR94G7hXpY=
|
||||
emojibase-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-4.0.0.tgz#3feb3e5bb5e5f2b6373b183b0f038c60889a9e29"
|
||||
integrity sha512-Yi4A1IxB7iZ+09Wqr2BEpHSQfugc5I8G+wckDOhCia0F7oOdErf/85jCwbpRQy7xtBbvlyS3xQrYedSeQot5Og==
|
||||
|
||||
emojibase-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-3.0.0.tgz#fc7a17aa20584df5a73619f06ac236b8a5bb452d"
|
||||
integrity sha512-iNDkbtn8UxKTxjIlvHLqfXovZaIulnuuyo2/emU+ZlPr2OmzxGfiDI+iIQ3WWqdlA6lgjrPNWgpbytt4lnJYrg==
|
||||
|
||||
emojis-list@^2.0.0:
|
||||
version "2.1.0"
|
||||
|
|
Loading…
Reference in New Issue