From d8f4512439bc844e5df75050afb67aef0d578f72 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 22 May 2019 20:41:27 +0200 Subject: [PATCH 001/413] add basic spoiler support --- res/css/views/rooms/_EventTile.scss | 27 +++++++++++++++++ src/HtmlUtils.js | 2 +- src/components/views/elements/Spoiler.js | 32 ++++++++++++++++++++ src/components/views/messages/TextualBody.js | 30 ++++++++++++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/components/views/elements/Spoiler.js diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 72881231f8..dd078d7f30 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -280,6 +280,33 @@ limitations under the License. overflow-y: hidden; } +/* Spoiler stuff */ +.mx_EventTile_spoiler { + cursor: pointer; +} + +.mx_EventTile_spoiler_reason { + color: $event-timestamp-color; + font-size: 11px; +} + +.mx_EventTile_spoiler_content { + background-color: black; +} + +.mx_EventTile_spoiler_content > span { + visibility: hidden; +} + + +.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { + background-color: initial; +} + +.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content > span { + visibility: visible; +} + /* End to end encryption stuff */ .mx_EventTile:hover .mx_EventTile_e2eIcon { opacity: 1; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index d06c31682d..626b228357 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -250,7 +250,7 @@ const sanitizeHtmlParams = { allowedAttributes: { // custom ones first: font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix - span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix + span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix img: ['src', 'width', 'height', 'alt', 'title'], ol: ['start'], diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.js new file mode 100644 index 0000000000..e1e0febbab --- /dev/null +++ b/src/components/views/elements/Spoiler.js @@ -0,0 +1,32 @@ +'use strict'; + +import React from 'react'; + +module.exports = React.createClass({ + displayName: 'Spoiler', + + getInitialState() { + return { + visible: false, + }; + }, + + toggleVisible() { + this.setState({ visible: !this.state.visible }); + }, + + render: function() { + const reason = this.props.reason ? ( + {"(" + this.props.reason + ")"} + ) : null; + return ( + + { reason } +   + + + + + ); + } +}) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 1fc16d6a53..e6d67b034d 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -96,6 +96,8 @@ module.exports = React.createClass({ }, _applyFormatting() { + this.activateSpoilers(this.refs.content.children); + // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer // are still sent as plaintext URLs. If these are ever pillified in the composer, // we should be pillify them here by doing the linkifying BEFORE the pillifying. @@ -184,6 +186,34 @@ module.exports = React.createClass({ } }, + activateSpoilers: function(nodes) { + let node = nodes[0]; + while (node) { + if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") { + const spoilerContainer = document.createElement('span'); + + const reason = node.getAttribute("data-mx-spoiler"); + const Spoiler = sdk.getComponent('elements.Spoiler'); + node.removeAttribute("data-mx-spoiler"); // we don't want to recurse + const spoiler = ; + + ReactDOM.render(spoiler, spoilerContainer); + node.parentNode.replaceChild(spoilerContainer, node); + + node = spoilerContainer; + } + + if (node.childNodes && node.childNodes.length) { + this.activateSpoilers(node.childNodes); + } + + node = node.nextSibling; + } + }, + pillifyLinks: function(nodes) { const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); let node = nodes[0]; From eddac4b188303d4fe456dd8150a32c17c6a41c28 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Tue, 11 Jun 2019 21:08:45 +0200 Subject: [PATCH 002/413] blur spoilers --- res/css/views/rooms/_EventTile.scss | 14 +++----------- src/components/views/elements/Spoiler.js | 4 +--- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index dd078d7f30..cf3e5b7985 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -291,20 +291,12 @@ limitations under the License. } .mx_EventTile_spoiler_content { - background-color: black; + filter: blur(5px) saturate(0.1) sepia(1); + transition-duration: 0.5s; } -.mx_EventTile_spoiler_content > span { - visibility: hidden; -} - - .mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { - background-color: initial; -} - -.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content > span { - visibility: visible; + filter: none; } /* End to end encryption stuff */ diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.js index e1e0febbab..d2de7d70e0 100644 --- a/src/components/views/elements/Spoiler.js +++ b/src/components/views/elements/Spoiler.js @@ -23,9 +23,7 @@ module.exports = React.createClass({ { reason }   - - - + ); } From d0f78e9d4449e1d2c23d54819663252d874dfe23 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Tue, 11 Jun 2019 22:13:47 +0200 Subject: [PATCH 003/413] stop propagation of click events on un-hiding the spoiler --- src/components/views/elements/Spoiler.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.js index d2de7d70e0..9be7bc7784 100644 --- a/src/components/views/elements/Spoiler.js +++ b/src/components/views/elements/Spoiler.js @@ -11,7 +11,12 @@ module.exports = React.createClass({ }; }, - toggleVisible() { + toggleVisible(e) { + if (!this.state.visible) { + // we are un-blurring, we don't want this click to propagate to potential child pills + e.preventDefault(); + e.stopPropagation(); + } this.setState({ visible: !this.state.visible }); }, From 62522b2b532bf8c0f47186f904927af7d301ac67 Mon Sep 17 00:00:00 2001 From: "ferhad.necef" Date: Wed, 31 Jul 2019 17:25:22 +0000 Subject: [PATCH 004/413] Translated using Weblate (Azerbaijani) Currently translated at 23.7% (401 of 1695 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/az/ --- src/i18n/strings/az.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/az.json b/src/i18n/strings/az.json index 1de6aff689..161c4d242a 100644 --- a/src/i18n/strings/az.json +++ b/src/i18n/strings/az.json @@ -478,5 +478,8 @@ "You cannot modify widgets in this room.": "Bu otaqda vidjetləri dəyişdirə bilməzsiniz.", "Verifies a user, device, and pubkey tuple": "Bir istifadəçini, cihazı və publik açarı yoxlayır", "Unknown (user, device) pair:": "Naməlum (istifadəçi, cihaz) qoşulma:", - "Verified key": "Təsdiqlənmiş açar" + "Verified key": "Təsdiqlənmiş açar", + "Sends the given message coloured as a rainbow": "Verilən mesajı göy qurşağı kimi rəngli göndərir", + "Sends the given emote coloured as a rainbow": "Göndərilmiş emote rəngini göy qurşağı kimi göndərir", + "Unrecognised command:": "Tanınmayan əmr:" } From e19a9688b470a330662efe8b5452a48317765281 Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Thu, 1 Aug 2019 15:08:00 +0000 Subject: [PATCH 005/413] Translated using Weblate (Bulgarian) Currently translated at 100.0% (1695 of 1695 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index dacb512f28..299b9497da 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2034,5 +2034,15 @@ "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Не можете да влезете в профила си. Свържете се с администратора на сървъра за повече информация.", "You're signed out": "Излязохте от профила", "Clear personal data": "Изчисти личните данни", - "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Внимание: личните ви данни (включително ключове за шифроване) все още се съхраняват на това устройство. Изчистете, ако сте приключили с използването на това устройство или искате да влезете в друг профил." + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Внимание: личните ви данни (включително ключове за шифроване) все още се съхраняват на това устройство. Изчистете, ако сте приключили с използването на това устройство или искате да влезете в друг профил.", + "Identity Server": "Сървър за самоличност", + "Integrations Manager": "Мениджър на интеграциите", + "Find others by phone or email": "Открийте други по телефон или имейл", + "Be found by phone or email": "Бъдете открит по телефон или имейл", + "Use bots, bridges, widgets and sticker packs": "Използвайте ботове, връзки с други мрежи, приспособления и стикери", + "Terms of Service": "Условия за ползване", + "To continue you need to accept the Terms of this service.": "За да продължите е необходимо да приемете условията за ползване на тази услуга.", + "Service": "Услуга", + "Summary": "Обобщение", + "Terms": "Условия" } From aceea0facfa4af9586bace411dab1aa72a2dcc77 Mon Sep 17 00:00:00 2001 From: Alexey Murz Korepov Date: Thu, 1 Aug 2019 10:49:14 +0000 Subject: [PATCH 006/413] Translated using Weblate (Russian) Currently translated at 100.0% (1695 of 1695 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 66 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index ef9c14a968..c934882031 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -1241,7 +1241,7 @@ "Whether or not you're logged in (we don't record your username)": "Независимо от того, вошли вы или нет (мы не записываем ваше имя пользователя)", "Unable to load! Check your network connectivity and try again.": "Не удалось загрузить! Проверьте подключение к сети и попробуйте снова.", "Failed to invite users to the room:": "Не удалось пригласить пользователей в комнату:", - "Upgrades a room to a new version": "Обновляет комнату до новой версии", + "Upgrades a room to a new version": "Модернизирует комнату до новой версии", "Sets the room name": "Устанавливает название комнаты", "Forces the current outbound group session in an encrypted room to be discarded": "Принудительно отбрасывает текущий сеанс исходящей группы в зашифрованной комнате", "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s модернизировал эту комнату.", @@ -1604,13 +1604,13 @@ "The server does not support the room version specified.": "Сервер не поддерживает указанную версию комнаты.", "Name or Matrix ID": "Имя или Matrix ID", "Email, name or Matrix ID": "Эл. почта, имя или Matrix ID", - "Room upgrade confirmation": "Подтверждение обновления комнаты", + "Room upgrade confirmation": "Подтверждение модернизации комнаты", "Upgrading a room can be destructive and isn't always necessary.": "Модернизация комнаты может быть разрушительной и не всегда необходимой.", - "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Обновления комнат обычно рекомендуются, если версия номера считается нестабильной. Нестабильные версии комнат могут содержать ошибки, отсутствующие функции или уязвимости безопасности.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Модернизация версий комнат обычно рекомендуются, если номер версии комнаты считается нестабильным. Нестабильные версии комнат могут содержать ошибки, отсутствующие функции или уязвимости безопасности.", "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Модернизация комнаты обычно влияет только на обработку серверной стороны комнаты. Если у вас возникли проблемы с вашим Riot-клиентом , пожалуйста, отправьте запрос в .", "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. 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.": "Предупреждение: Обновление номера не приведет к автоматическому переходу участников комнаты на новую версию комнаты. Мы разместим ссылку на новую комнату в старой версии комнаты - участники комнаты должны будут нажать эту ссылку для присоединения к новой комнате.", "Please confirm that you'd like to go forward with upgrading this room from to .": "Пожалуйста, подтвердите, что вы хотите перейти к обновлению этой комнаты с на .", - "Upgrade": "Обновление", + "Upgrade": "Модернизация", "Changes your avatar in this current room only": "Меняет ваш аватар только в этой комнате", "Unbans user with given ID": "Разблокирует пользователя с заданным ID", "Adds a custom widget by URL to the room": "Добавляет пользовательский виджет по URL-адресу в комнате", @@ -1907,5 +1907,61 @@ "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Вы можете войти в систему, но некоторые возможности не будет доступны, пока сервер идентификации не станет доступным. Если вы продолжаете видеть это предупреждение, проверьте вашу конфигурацию или свяжитесь с администратором сервера.", "Log in to your new account.": "Войти в вашу новую учётную запись.", "You can now close this window or log in to your new account.": "Вы можете закрыть это окно или войти в вашу новую учётную запись.", - "Registration Successful": "Регистрация успешно завершена" + "Registration Successful": "Регистрация успешно завершена", + "Changes your avatar in all rooms": "Изменить Ваш аватар во всех комнатах", + "%(senderName)s made no change.": "%(senderName)s не внёс изменений.", + "No integrations server configured": "Серверы интеграций не настроены", + "This Riot instance does not have an integrations server configured.": "Этот экземпляр Riot не имеет настроенных серверов интеграций.", + "Connecting to integrations server...": "Соединение с сервером интеграций...", + "Cannot connect to integrations server": "Ошибка соединения с сервером интеграций", + "The integrations server is offline or it cannot reach your homeserver.": "Сервер интеграций недоступен или не может связаться с вашим сервером.", + "Loading room preview": "Загрузка предпросмотра комнаты", + "Failed to connect to integrations server": "Ошибка соединения с сервером интеграций", + "No integrations server is configured to manage stickers with": "Нет серверов интеграций, настроенных для управления стикерами", + "Agree": "Согласен", + "Disagree": "Не согласен", + "Happy": "Радует", + "Party Popper": "Хлопушка", + "Confused": "Смушён", + "Eyes": "Глаза", + "Show all": "Показать все", + "Edited at %(date)s. Click to view edits.": "Отредактировано в %(date)s. Нажмите, чтобы посмотреть историю редактирования.", + "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)sне внёс изменений %(count)s раз", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)sне внёс изменений", + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sне внёс изменений %(count)s раз", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sне внёс изменений", + "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Пожалуйста, расскажите нам что пошло не так, либо, ещё лучше, создайте отчёт в GitHub с описанием проблемы.", + "Removing…": "Удаление...", + "Clear all data on this device?": "Очистить все данные на этом устройстве?", + "Clearing all data from this device is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Очистка данных на этом устройстве необратима. Шифрованные сообщения будут утеряны, если не было сделано резервной копии их ключей шифрования.", + "Clear all data": "Очистить все данные", + "Your homeserver doesn't seem to support this feature.": "Ваш сервер похоже не поддерживает эту возможность.", + "Message edits": "Правки сообщения", + "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "Модернизация этой комнаты требует закрытие комнаты в текущем состояние и создания новой комнаты вместо неё. Чтобы упростить процесс для участников, будет сделано:", + "Identity Server": "Сервер идентификаций", + "Integrations Manager": "Менеджер интеграций", + "Find others by phone or email": "Найти других по номеру телефона или email", + "Be found by phone or email": "Будут найдены по номеру телефона или email", + "Use bots, bridges, widgets and sticker packs": "Использовать боты, мосты, виджеты и наборы стикеров", + "Terms of Service": "Условия использования", + "To continue you need to accept the Terms of this service.": "Для продолжения вы должны принять Условия использования.", + "Service": "Сервис", + "Summary": "Сводка", + "Terms": "Условия", + "Upload all": "Загрузить всё", + "Resend edit": "Отправить исправление повторно", + "Resend %(unsentCount)s reaction(s)": "Отправить повторно %(unsentCount)s реакций", + "Resend removal": "Отправить удаление повторно", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Ваша новая учётная запись (%(newAccountId)s) зарегистрирована, но Вы уже вошли в другую учётную запись (%(loggedInUserId)s).", + "Continue with previous account": "Продолжить с предыдущей учётной записью", + "Failed to re-authenticate due to a homeserver problem": "Ошибка повторной аутентификации из-за проблем на сервере", + "Failed to re-authenticate": "Ошибка повторной аутентификации", + "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Восстановить доступ к Вашей учётной записи и восстановить ключи шифрования, сохранённые на этом устройстве. Без них Вы не сможете читать все ваши зашифрованные сообщения на любом устройстве.", + "Enter your password to sign in and regain access to your account.": "Введите Ваш пароль для входа и восстановления доступа к Вашей учётной записи.", + "Forgotten your password?": "Забыли Ваш пароль?", + "Sign in and regain access to your account.": "Войти и восстановить доступ к Вашей учётной записи.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Вы не можете войти в Вашу учётную запись. Пожалуйста свяжитесь с администратором вашего сервера для более подробной информации.", + "You're signed out": "Вы вышли из учётной записи", + "Clear personal data": "Очистить персональные данные", + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Внимание: Ваши персональные данные (включая ключи шифрования) всё ещё хранятся на этом устройстви. Очистите их, если Вы закончили использовать это устройство, либо хотите войти в другую учётную запись." } From a588f8cf9149b9d8b63fd20995de40325102c6cc Mon Sep 17 00:00:00 2001 From: RainSlide Date: Sun, 4 Aug 2019 19:05:42 +0000 Subject: [PATCH 007/413] Translated using Weblate (Chinese (Simplified)) Currently translated at 89.4% (1515 of 1695 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hans/ --- src/i18n/strings/zh_Hans.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 3dc5e1fc22..1536a4d2ab 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -1030,7 +1030,7 @@ "Enter keywords separated by a comma:": "输入以逗号间隔的关键词:", "Forward Message": "转发消息", "You have successfully set a password and an email address!": "您已经成功设置了密码和邮箱地址!", - "Remove %(name)s from the directory?": "从目录中移除 %(name)s 吗?", + "Remove %(name)s from the directory?": "是否从目录中移除 %(name)s?", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot 使用了许多先进的浏览器功能,有些在你目前所用的浏览器上无法使用或仅为实验性的功能。", "Developer Tools": "开发者工具", "Preparing to send logs": "准备发送日志", @@ -1782,7 +1782,7 @@ "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.": "位于 %(widgetUrl)s 的小部件想要验证您的身份。在您允许后,小部件就可以验证您的用户 ID,但不能代您执行操作。", "Remember my selection for this widget": "记住我对此小部件的选择", "Deny": "拒绝", - "Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot 无法从主服务器获取协议列表。该主服务器可能太旧了而不支持第三方网络。", + "Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot 无法从主服务器处获取协议列表。该主服务器上的软件可能过旧,不支持第三方网络。", "Riot failed to get the public room list.": "Riot 无法获取公开聊天室列表。", "The homeserver may be unavailable or overloaded.": "主服务器似乎不可用或过载。", "You have %(count)s unread notifications in a prior version of this room.|other": "您在此聊天室的先前版本中有 %(count)s 条未读通知。", From d4ef4e3d3ff5e50d56e9e6d28afc910619ae7423 Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Mon, 5 Aug 2019 08:24:31 +0000 Subject: [PATCH 008/413] Translated using Weblate (Finnish) Currently translated at 99.6% (1689 of 1695 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 9a9375af7b..5483586128 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -1942,5 +1942,15 @@ "Sign in and regain access to your account.": "Kirjaudu ja pääse takaisin tilillesi.", "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Et voi kirjautua tilillesi. Ota yhteyttä kotipalvelimesi ylläpitäjään saadaksesi lisätietoja.", "Clear personal data": "Poista henkilökohtaiset tiedot", - "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Varoitus: Henkilökohtaisia tietojasi (mukaan lukien salausavaimia) on edelleen tallennettuna tällä laitteella. Poista ne, jos et aio enää käyttää tätä laitetta, tai haluat kirjautua toiselle tilille." + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Varoitus: Henkilökohtaisia tietojasi (mukaan lukien salausavaimia) on edelleen tallennettuna tällä laitteella. Poista ne, jos et aio enää käyttää tätä laitetta, tai haluat kirjautua toiselle tilille.", + "Identity Server": "Identiteettipalvelin", + "Integrations Manager": "Integraatioiden hallinta", + "Find others by phone or email": "Löydä muita käyttäjiä puhelimen tai sähköpostin perusteella", + "Be found by phone or email": "Varmista, että sinut löydetään puhelimen tai sähköpostin perusteella", + "Use bots, bridges, widgets and sticker packs": "Käytä botteja, siltoja, sovelmia ja tarrapaketteja", + "Terms of Service": "Käyttöehdot (Terms of Service)", + "To continue you need to accept the Terms of this service.": "Jatkaaksesi sinun täytyy hyväksyä palvelun ehdot.", + "Service": "Palvelu", + "Summary": "Yhteenveto", + "Terms": "Ehdot" } From 9bbfb9c77c605f1c45e829045f0338af3e73a8ed Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 5 Aug 2019 13:07:00 +0000 Subject: [PATCH 009/413] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1696 of 1696 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index b880f361d0..2d1a801fe8 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2065,5 +2065,6 @@ "To continue you need to accept the Terms of this service.": "要繼續,您必須接受本服務條款。", "Service": "服務", "Summary": "摘要", - "Terms": "條款" + "Terms": "條款", + "This account has been deactivated.": "此帳號已停用。" } From aa0a2a579ea4a88ccc76e001f811cb2dc1b0096b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 6 Aug 2019 12:43:30 +0100 Subject: [PATCH 010/413] Add copyright headers --- res/css/views/settings/_EmailAddresses.scss | 1 + res/css/views/settings/_PhoneNumbers.scss | 1 + src/components/views/settings/EmailAddresses.js | 1 + src/components/views/settings/PhoneNumbers.js | 1 + 4 files changed, 4 insertions(+) diff --git a/res/css/views/settings/_EmailAddresses.scss b/res/css/views/settings/_EmailAddresses.scss index d7606ecea9..1c9ce724d1 100644 --- a/res/css/views/settings/_EmailAddresses.scss +++ b/res/css/views/settings/_EmailAddresses.scss @@ -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. diff --git a/res/css/views/settings/_PhoneNumbers.scss b/res/css/views/settings/_PhoneNumbers.scss index 7aaef2a56b..d88ed176aa 100644 --- a/res/css/views/settings/_PhoneNumbers.scss +++ b/res/css/views/settings/_PhoneNumbers.scss @@ -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. diff --git a/src/components/views/settings/EmailAddresses.js b/src/components/views/settings/EmailAddresses.js index 1bb41ae8b5..4702240331 100644 --- a/src/components/views/settings/EmailAddresses.js +++ b/src/components/views/settings/EmailAddresses.js @@ -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. diff --git a/src/components/views/settings/PhoneNumbers.js b/src/components/views/settings/PhoneNumbers.js index 4ebc2a20a6..a2769d843a 100644 --- a/src/components/views/settings/PhoneNumbers.js +++ b/src/components/views/settings/PhoneNumbers.js @@ -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. From accb1eea9fe87b59f40cd007856d1b7d76b21e31 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 6 Aug 2019 13:03:15 +0100 Subject: [PATCH 011/413] Move existing 3PID settings UX to account directory --- .../settings/{ => account}/EmailAddresses.js | 16 ++++++++-------- .../views/settings/{ => account}/PhoneNumbers.js | 16 ++++++++-------- .../settings/tabs/user/GeneralUserSettingsTab.js | 6 ++++-- 3 files changed, 20 insertions(+), 18 deletions(-) rename src/components/views/settings/{ => account}/EmailAddresses.js (95%) rename src/components/views/settings/{ => account}/PhoneNumbers.js (95%) diff --git a/src/components/views/settings/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js similarity index 95% rename from src/components/views/settings/EmailAddresses.js rename to src/components/views/settings/account/EmailAddresses.js index 4702240331..b610b5ab64 100644 --- a/src/components/views/settings/EmailAddresses.js +++ b/src/components/views/settings/account/EmailAddresses.js @@ -17,14 +17,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t} from "../../../languageHandler"; -import MatrixClientPeg from "../../../MatrixClientPeg"; -import Field from "../elements/Field"; -import AccessibleButton from "../elements/AccessibleButton"; -import * as Email from "../../../email"; -import AddThreepid from "../../../AddThreepid"; -const sdk = require('../../../index'); -const Modal = require("../../../Modal"); +import {_t} from "../../../../languageHandler"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import Field from "../../elements/Field"; +import AccessibleButton from "../../elements/AccessibleButton"; +import * as Email from "../../../../email"; +import AddThreepid from "../../../../AddThreepid"; +const sdk = require('../../../../index'); +const Modal = require("../../../../Modal"); /* TODO: Improve the UX for everything in here. diff --git a/src/components/views/settings/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js similarity index 95% rename from src/components/views/settings/PhoneNumbers.js rename to src/components/views/settings/account/PhoneNumbers.js index a2769d843a..cabe4aef86 100644 --- a/src/components/views/settings/PhoneNumbers.js +++ b/src/components/views/settings/account/PhoneNumbers.js @@ -17,14 +17,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t} from "../../../languageHandler"; -import MatrixClientPeg from "../../../MatrixClientPeg"; -import Field from "../elements/Field"; -import AccessibleButton from "../elements/AccessibleButton"; -import AddThreepid from "../../../AddThreepid"; -import CountryDropdown from "../auth/CountryDropdown"; -const sdk = require('../../../index'); -const Modal = require("../../../Modal"); +import {_t} from "../../../../languageHandler"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import Field from "../../elements/Field"; +import AccessibleButton from "../../elements/AccessibleButton"; +import AddThreepid from "../../../../AddThreepid"; +import CountryDropdown from "../../auth/CountryDropdown"; +const sdk = require('../../../../index'); +const Modal = require("../../../../Modal"); /* TODO: Improve the UX for everything in here. diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 06c012c91e..8283d26ef1 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -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,8 +18,6 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; import ProfileSettings from "../../ProfileSettings"; -import EmailAddresses from "../../EmailAddresses"; -import PhoneNumbers from "../../PhoneNumbers"; import Field from "../../../elements/Field"; import * as languageHandler from "../../../../../languageHandler"; import {SettingLevel} from "../../../../../settings/SettingsStore"; @@ -110,6 +109,9 @@ export default class GeneralUserSettingsTab extends React.Component { _renderAccountSection() { const ChangePassword = sdk.getComponent("views.settings.ChangePassword"); + const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses"); + const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers"); + const passwordChangeForm = ( Date: Mon, 5 Aug 2019 19:07:16 +0000 Subject: [PATCH 012/413] Translated using Weblate (Bulgarian) Currently translated at 100.0% (1696 of 1696 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 299b9497da..4fc19262a0 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2044,5 +2044,6 @@ "To continue you need to accept the Terms of this service.": "За да продължите е необходимо да приемете условията за ползване на тази услуга.", "Service": "Услуга", "Summary": "Обобщение", - "Terms": "Условия" + "Terms": "Условия", + "This account has been deactivated.": "Този акаунт е деактивиран." } From d7fff4db8c0068d1d43a032adef1f657176cbe58 Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Mon, 5 Aug 2019 18:58:48 +0000 Subject: [PATCH 013/413] Translated using Weblate (Dutch) Currently translated at 100.0% (1696 of 1696 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index e4a0437b21..c73bbbf6a4 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1138,7 +1138,7 @@ "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.": "Foutopsporingslogboeken bevatten gebruiksgegevens over de toepassing, inclusief uw gebruikersnaam, de ID’s of bijnamen van de gesprekken en groepen die u heeft bezocht, evenals de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.", "Failed to send logs: ": "Versturen van logboeken mislukt: ", "Notes:": "Notities:", - "Preparing to send logs": "Logboeken worden voorbereid voor verzending", + "Preparing to send logs": "Logboeken worden voorbereid voor versturen", "e.g. %(exampleValue)s": "bv. %(exampleValue)s", "Every page you use in the app": "Iedere bladzijde die u in de toepassing gebruikt", "e.g. ": "bv. ", @@ -1831,7 +1831,7 @@ "Re-join": "Opnieuw toetreden", "You were banned from %(roomName)s by %(memberName)s": "U bent uit %(roomName)s verbannen door %(memberName)s", "Something went wrong with your invite to %(roomName)s": "Er is iets misgegaan met uw uitnodiging voor %(roomName)s", - "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "De foutcode %(errcode)s werd weergegeven bij het valideren van uw uitnodiging. U kunt deze informatie aan een gespreksadministrator doorgeven.", + "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "De foutcode %(errcode)s werd weergegeven bij het valideren van uw uitnodiging. U kunt deze informatie aan een gespreksbeheerder doorgeven.", "You can only join it with a working invite.": "U kunt het gesprek enkel toetreden met een werkende uitnodiging.", "You can still join it because this is a public room.": "U kunt het nog steeds toetreden, aangezien het een openbaar gesprek is.", "Join the discussion": "Neem deel aan het gesprek", @@ -1954,18 +1954,19 @@ "Your homeserver (%(domainName)s) admin has signed you out of your account %(displayName)s (%(userId)s).": "De beheerder van uw thuisserver (%(domainName)s) heeft u afgemeld van uw account %(displayName)s%(userId)s.", "Clear personal data": "Persoonlijke gegevens wissen", "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Let op: uw persoonlijke gegevens (inclusief versleutelingssleutels) worden nog steeds op dit apparaat opgeslagen. Wis deze wanneer u klaar bent met het apparaat te gebruiken, of wanneer u zich wilt aanmelden met een andere account.", - "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Laat ons weten wat er verkeerd gelopen is of nog beter: maak een GitHub rapport dat het probleem beschrijft.", - "Identity Server": "Identiteit server", + "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Laat ons weten wat er verkeerd is gegaan, of nog beter, maak een foutrapport aan op GitHub, waarin u het probleem beschrijft.", + "Identity Server": "Identiteitsserver", "Integrations Manager": "Integratiebeheerder", - "Find others by phone or email": "Vind andere personen via telefoon of email", - "Be found by phone or email": "Word gevonden per telefoon of email", - "Use bots, bridges, widgets and sticker packs": "Gebruik robots, bruggen, widgets en sticker packs", + "Find others by phone or email": "Vind anderen via telefoonnummer of e-mailadres", + "Be found by phone or email": "Word gevonden via telefoonnummer of e-mailadres", + "Use bots, bridges, widgets and sticker packs": "Gebruik robots, bruggen, widgets en stickerpakketten", "Terms of Service": "Gebruiksvoorwaarden", - "To continue you need to accept the Terms of this service.": "Om verder te gaan moet je de gebruiksvoorwaarden van deze dienst aanvaarden.", + "To continue you need to accept the Terms of this service.": "Om verder te gaan dient u de gebruiksvoorwaarden van deze dienst te aanvaarden.", "Service": "Dienst", "Summary": "Samenvatting", "Terms": "Voorwaarden", - "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Krijg terug toegang tot uw account en herstel encryptiesleutels die opgeslagen zijn op dit apparaat. Zonder deze sleutels zal je versleutelde berichten niet kunnen lezen op andere apparaten.", - "Sign in and regain access to your account.": "Log in en krijg terug toegang tot je account.", - "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Je kan niet inloggen op je account. Gelieve je homeserver beheerder te contacteren voor meer informatie." + "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Verkrijg opnieuw toegang tot uw account en herstel de versleutelingssleutels die opgeslagen zijn op dit apparaat. Zonder deze sleutels zult u uw versleutelde berichten niet kunnen lezen op andere apparaten.", + "Sign in and regain access to your account.": "Meld u aan en verkrijg opnieuw toegang tot uw account.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt zich niet aanmelden met uw account. Neem contact op met de beheerder van uw thuisserver voor meer informatie.", + "This account has been deactivated.": "Deze account is gedeactiveerd." } From 4aaf67d51487c373548a240192c2a1d3f2414d78 Mon Sep 17 00:00:00 2001 From: Tirifto Date: Mon, 5 Aug 2019 18:51:57 +0000 Subject: [PATCH 014/413] Translated using Weblate (Esperanto) Currently translated at 99.9% (1695 of 1696 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index a4dc2dc061..6acab3900f 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -1906,5 +1906,15 @@ "Your Modular server": "Via Modular-servilo", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "Enigu la lokon de via Modular-hejmservilo. Ĝi povas uzi vian propran domajnan nomon aŭ esti subdomajno de modular.im.", "Invalid base_url for m.homeserver": "Nevalida base_url por m.homeserver", - "Invalid base_url for m.identity_server": "Nevalida base_url por m.identity_server" + "Invalid base_url for m.identity_server": "Nevalida base_url por m.identity_server", + "Identity Server": "Identiga servilo", + "Find others by phone or email": "Trovu aliajn per telefonnumero aŭ retpoŝtadreso", + "Be found by phone or email": "Troviĝu per telefonnumero aŭ retpoŝtadreso", + "Use bots, bridges, widgets and sticker packs": "Uzu robotojn, pontojn, fenestraĵojn, kaj glumarkarojn", + "Terms of Service": "Uzokondiĉoj", + "To continue you need to accept the Terms of this service.": "Por pluiĝi, vi devas akcepti la uzokondiĉojn.", + "Service": "Servo", + "Summary": "Resumo", + "Terms": "Kondiĉoj", + "This account has been deactivated.": "Tiu ĉi konto malaktiviĝis." } From 06649d6d5459c8dcb7844b5a9382128be3d864e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Tue, 6 Aug 2019 06:41:04 +0000 Subject: [PATCH 015/413] Translated using Weblate (French) Currently translated at 100.0% (1696 of 1696 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 156714ee47..df292e8957 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2072,5 +2072,6 @@ "To continue you need to accept the Terms of this service.": "Pour continuer vous devez accepter les conditions de ce service.", "Service": "Service", "Summary": "Résumé", - "Terms": "Conditions" + "Terms": "Conditions", + "This account has been deactivated.": "Ce compte a été désactivé." } From e163df17e2860dde1c1a9093a3a230d0452c74ff Mon Sep 17 00:00:00 2001 From: Walter Date: Tue, 6 Aug 2019 07:24:47 +0000 Subject: [PATCH 016/413] Translated using Weblate (Russian) Currently translated at 100.0% (1696 of 1696 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index c934882031..c37a7aa43f 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -1931,7 +1931,7 @@ "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sне внёс изменений %(count)s раз", "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sне внёс изменений", "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Пожалуйста, расскажите нам что пошло не так, либо, ещё лучше, создайте отчёт в GitHub с описанием проблемы.", - "Removing…": "Удаление...", + "Removing…": "Удаление…", "Clear all data on this device?": "Очистить все данные на этом устройстве?", "Clearing all data from this device is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Очистка данных на этом устройстве необратима. Шифрованные сообщения будут утеряны, если не было сделано резервной копии их ключей шифрования.", "Clear all data": "Очистить все данные", @@ -1963,5 +1963,6 @@ "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Вы не можете войти в Вашу учётную запись. Пожалуйста свяжитесь с администратором вашего сервера для более подробной информации.", "You're signed out": "Вы вышли из учётной записи", "Clear personal data": "Очистить персональные данные", - "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Внимание: Ваши персональные данные (включая ключи шифрования) всё ещё хранятся на этом устройстви. Очистите их, если Вы закончили использовать это устройство, либо хотите войти в другую учётную запись." + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Внимание: Ваши персональные данные (включая ключи шифрования) всё ещё хранятся на этом устройстви. Очистите их, если Вы закончили использовать это устройство, либо хотите войти в другую учётную запись.", + "This account has been deactivated.": "Этот аккаунт был деактивирован." } From 35d2aac269f8f23ffd91b505937cabc250ca4aad Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Mon, 5 Aug 2019 19:23:24 +0000 Subject: [PATCH 017/413] Translated using Weblate (West Flemish) Currently translated at 100.0% (1696 of 1696 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/vls/ --- src/i18n/strings/vls.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/vls.json b/src/i18n/strings/vls.json index 680f761029..e8422febcd 100644 --- a/src/i18n/strings/vls.json +++ b/src/i18n/strings/vls.json @@ -1691,5 +1691,24 @@ "Resend %(unsentCount)s reaction(s)": "%(unsentCount)s reactie(s) herverstuurn", "Resend removal": "Verwyderienge herverstuurn", "Failed to re-authenticate due to a homeserver problem": "’t Heranmeldn is mislukt omwille van e probleem me de thuusserver", - "Failed to re-authenticate": "’t Heranmeldn is mislukt" + "Failed to re-authenticate": "’t Heranmeldn is mislukt", + "Identity Server": "Identiteitsserver", + "Integrations Manager": "Integroatiebeheerder", + "Find others by phone or email": "Viendt andere menschn via hunder telefongnumero of e-mailadresse", + "Be found by phone or email": "Wor gevoundn via je telefongnumero of e-mailadresse", + "Use bots, bridges, widgets and sticker packs": "Gebruukt robottn, bruggn, widgets en stickerpakkettn", + "Terms of Service": "Gebruuksvoorwoardn", + "To continue you need to accept the Terms of this service.": "Vo voort te goan moe je de gebruuksvoorwoardn van deezn dienst anveirdn.", + "Service": "Dienst", + "Summary": "Soamnvattienge", + "Terms": "Voorwoardn", + "This account has been deactivated.": "Deezn account is gedeactiveerd gewist.", + "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Herkrygt den toegank tou jen account en herstelt de versleuteriengssleuters dan ip dit toestel ipgesloagn gewist zyn. Zounder deze sleuters goa je je versleuterde berichtn nie kunn leezn ip andere toestelln.", + "Enter your password to sign in and regain access to your account.": "Voert je paswoord in vo jen an te meldn en den toegank tou jen account te herkrygn.", + "Forgotten your password?": "Paswoord vergeetn?", + "Sign in and regain access to your account.": "Meldt jen heran en herkrygt den toegank tou jen account.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Je ku je nie anmeldn me jen account. Nimt contact ip me de beheerder van je thuusserver vo meer informoatie.", + "You're signed out": "Je zyt afgemeld", + "Clear personal data": "Persoonlike gegeevns wissn", + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Let ip: je persoonlike gegeevns (inclusief versleuteriengssleuters) wordn nog alsan ip dit toestel ipgesloagn. Wist ze a je gereed zyt me ’t toestel te gebruukn, of a je je wilt anmeldn me nen andern account." } From 178d6605c4fd11e9946a29660698ecd722ac9f75 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 8 Aug 2019 11:35:35 +0100 Subject: [PATCH 018/413] Add controls for toggling discovery in user settings This adds controls for each 3PID to allow the user to choose whether it's bound on the IS. Fixes https://github.com/vector-im/riot-web/issues/10159 --- res/css/views/settings/_PhoneNumbers.scss | 9 + .../views/settings/account/PhoneNumbers.js | 2 +- .../settings/discovery/EmailAddresses.js | 248 ++++++++++++++++ .../views/settings/discovery/PhoneNumbers.js | 267 ++++++++++++++++++ .../tabs/user/GeneralUserSettingsTab.js | 18 ++ src/i18n/strings/en_EN.json | 46 +-- 6 files changed, 572 insertions(+), 18 deletions(-) create mode 100644 src/components/views/settings/discovery/EmailAddresses.js create mode 100644 src/components/views/settings/discovery/PhoneNumbers.js diff --git a/res/css/views/settings/_PhoneNumbers.scss b/res/css/views/settings/_PhoneNumbers.scss index d88ed176aa..507b07334e 100644 --- a/res/css/views/settings/_PhoneNumbers.scss +++ b/res/css/views/settings/_PhoneNumbers.scss @@ -37,6 +37,15 @@ limitations under the License. margin-left: 5px; } +.mx_ExistingPhoneNumber_verification { + display: inline-flex; + align-items: center; + + .mx_Field { + margin: 0 0 0 1em; + } +} + .mx_PhoneNumbers_input { display: flex; align-items: center; diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js index cabe4aef86..d892f17ff8 100644 --- a/src/components/views/settings/account/PhoneNumbers.js +++ b/src/components/views/settings/account/PhoneNumbers.js @@ -225,7 +225,7 @@ export default class PhoneNumbers extends React.Component {
{_t("A text message has been sent to +%(msisdn)s. " + - "Please enter the verification code it contains", { msisdn: msisdn })} + "Please enter the verification code it contains.", { msisdn: msisdn })}
{this.state.verifyError}
diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js new file mode 100644 index 0000000000..7862eda61e --- /dev/null +++ b/src/components/views/settings/discovery/EmailAddresses.js @@ -0,0 +1,248 @@ +/* +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { _t } from "../../../../languageHandler"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import sdk from '../../../../index'; +import Modal from '../../../../Modal'; +import IdentityAuthClient from '../../../../IdentityAuthClient'; +import AddThreepid from '../../../../AddThreepid'; + +/* +TODO: Improve the UX for everything in here. +It's very much placeholder, but it gets the job done. The old way of handling +email addresses in user settings was to use dialogs to communicate state, however +due to our dialog system overriding dialogs (causing unmounts) this creates problems +for a sane UX. For instance, the user could easily end up entering an email address +and receive a dialog to verify the address, which then causes the component here +to forget what it was doing and ultimately fail. Dialogs are still used in some +places to communicate errors - these should be replaced with inline validation when +that is available. +*/ + +/* +TODO: Reduce all the copying between account vs. discovery components. +*/ + +export class EmailAddress extends React.Component { + static propTypes = { + email: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + const { bound } = props.email; + + this.state = { + verifying: false, + addTask: null, + continueDisabled: false, + bound, + }; + } + + async changeBinding({ bind, label, errorTitle }) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const { medium, address } = this.props.email; + + const task = new AddThreepid(); + this.setState({ + verifying: true, + continueDisabled: true, + addTask: task, + }); + + try { + // XXX: Unfortunately, at the moment we can't just bind via the HS + // in a single operation, at it will error saying the 3PID is in use + // even though it's in use by the current user. For the moment, we + // work around this by removing the 3PID from the HS and re-adding + // it with IS binding enabled. + // See https://github.com/matrix-org/matrix-doc/pull/2140/files#r311462052 + await MatrixClientPeg.get().deleteThreePid(medium, address); + await task.addEmailAddress(address, bind); + this.setState({ + continueDisabled: false, + bound: bind, + }); + } catch (err) { + console.error(`Unable to ${label} email address ${address} ${err}`); + this.setState({ + verifying: false, + continueDisabled: false, + addTask: null, + }); + Modal.createTrackedDialog(`Unable to ${label} email address`, '', ErrorDialog, { + title: errorTitle, + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + } + + onRevokeClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.changeBinding({ + bind: false, + label: "revoke", + errorTitle: _t("Unable to revoke sharing for email address"), + }); + } + + onShareClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.changeBinding({ + bind: true, + label: "share", + errorTitle: _t("Unable to share email address"), + }); + } + + onContinueClick = async (e) => { + e.stopPropagation(); + e.preventDefault(); + + this.setState({ continueDisabled: true }); + try { + await this.state.addTask.checkEmailLinkClicked(); + this.setState({ + addTask: null, + continueDisabled: false, + verifying: false, + }); + } catch (err) { + this.setState({ continueDisabled: false }); + if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to verify email address: " + err); + Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, { + title: _t("Unable to verify email address."), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + } + } + + render() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const { address } = this.props.email; + const { verifying, bound } = this.state; + + let status; + if (verifying) { + status = + {_t("Check your inbox, then click Continue")} + + {_t("Continue")} + + ; + } else if (bound) { + status = + {_t("Revoke")} + ; + } else { + status = + {_t("Share")} + ; + } + + return ( +
+ {address} + {status} +
+ ); + } +} + +export default class EmailAddresses extends React.Component { + constructor() { + super(); + + this.state = { + loaded: false, + emails: [], + }; + } + + async componentWillMount() { + const client = MatrixClientPeg.get(); + const userId = client.getUserId(); + + const { threepids } = await client.getThreePids(); + const emails = threepids.filter((a) => a.medium === 'email'); + + if (emails.length > 0) { + // TODO: Handle terms agreement + // See https://github.com/vector-im/riot-web/issues/10522 + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken(); + + // Restructure for lookup query + const query = emails.map(({ medium, address }) => [medium, address]); + const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); + + // Record which are already bound + for (const [medium, address, mxid] of lookupResults.threepids) { + if (medium !== "email" || mxid !== userId) { + continue; + } + const email = emails.find(e => e.address === address); + if (!email) continue; + email.bound = true; + } + } + + this.setState({ emails }); + } + + render() { + let content; + if (this.state.emails.length > 0) { + content = this.state.emails.map((e) => { + return ; + }); + } else { + content = + {_t("Discovery options will appear once you have added an email above.")} + ; + } + + return ( +
+ {content} +
+ ); + } +} diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js new file mode 100644 index 0000000000..3930277aea --- /dev/null +++ b/src/components/views/settings/discovery/PhoneNumbers.js @@ -0,0 +1,267 @@ +/* +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { _t } from "../../../../languageHandler"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import sdk from '../../../../index'; +import Modal from '../../../../Modal'; +import IdentityAuthClient from '../../../../IdentityAuthClient'; +import AddThreepid from '../../../../AddThreepid'; + +/* +TODO: Improve the UX for everything in here. +This is a copy/paste of EmailAddresses, mostly. + */ + +// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic + +export class PhoneNumber extends React.Component { + static propTypes = { + msisdn: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + const { bound } = props.msisdn; + + this.state = { + verifying: false, + verificationCode: "", + addTask: null, + continueDisabled: false, + bound, + }; + } + + async changeBinding({ bind, label, errorTitle }) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const { medium, address } = this.props.msisdn; + + const task = new AddThreepid(); + this.setState({ + verifying: true, + continueDisabled: true, + addTask: task, + }); + + try { + // XXX: Unfortunately, at the moment we can't just bind via the HS + // in a single operation, at it will error saying the 3PID is in use + // even though it's in use by the current user. For the moment, we + // work around this by removing the 3PID from the HS and re-adding + // it with IS binding enabled. + // See https://github.com/matrix-org/matrix-doc/pull/2140/files#r311462052 + await MatrixClientPeg.get().deleteThreePid(medium, address); + // XXX: Sydent will accept a number without country code if you add + // a leading plus sign to a number in E.164 format (which the 3PID + // address is), but this goes against the spec. + // See https://github.com/matrix-org/matrix-doc/issues/2222 + await task.addMsisdn(null, `+${address}`, bind); + this.setState({ + continueDisabled: false, + bound: bind, + }); + } catch (err) { + console.error(`Unable to ${label} phone number ${address} ${err}`); + this.setState({ + verifying: false, + continueDisabled: false, + addTask: null, + }); + Modal.createTrackedDialog(`Unable to ${label} phone number`, '', ErrorDialog, { + title: errorTitle, + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + } + + onRevokeClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.changeBinding({ + bind: false, + label: "revoke", + errorTitle: _t("Unable to revoke sharing for phone number"), + }); + } + + onShareClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.changeBinding({ + bind: true, + label: "share", + errorTitle: _t("Unable to share phone number"), + }); + } + + onVerificationCodeChange = (e) => { + this.setState({ + verificationCode: e.target.value, + }); + } + + onContinueClick = async (e) => { + e.stopPropagation(); + e.preventDefault(); + + this.setState({ continueDisabled: true }); + const token = this.state.verificationCode; + try { + await this.state.addTask.haveMsisdnToken(token); + this.setState({ + addTask: null, + continueDisabled: false, + verifying: false, + verifyError: null, + verificationCode: "", + }); + } catch (err) { + this.setState({ continueDisabled: false }); + if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to verify phone number: " + err); + Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, { + title: _t("Unable to verify phone number."), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } else { + this.setState({verifyError: _t("Incorrect verification code")}); + } + } + } + + render() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const Field = sdk.getComponent('elements.Field'); + const { address } = this.props.msisdn; + const { verifying, bound } = this.state; + + let status; + if (verifying) { + status = + + {_t("Please enter verification code sent via text.")} +
+ {this.state.verifyError} +
+
+ + +
; + } else if (bound) { + status = + {_t("Revoke")} + ; + } else { + status = + {_t("Share")} + ; + } + + return ( +
+ +{address} + {status} +
+ ); + } +} + +export default class PhoneNumbers extends React.Component { + constructor() { + super(); + + this.state = { + loaded: false, + msisdns: [], + }; + } + + async componentWillMount() { + const client = MatrixClientPeg.get(); + const userId = client.getUserId(); + + const { threepids } = await client.getThreePids(); + const msisdns = threepids.filter((a) => a.medium === 'msisdn'); + + if (msisdns.length > 0) { + // TODO: Handle terms agreement + // See https://github.com/vector-im/riot-web/issues/10522 + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken(); + + // Restructure for lookup query + const query = msisdns.map(({ medium, address }) => [medium, address]); + const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); + + // Record which are already bound + for (const [medium, address, mxid] of lookupResults.threepids) { + if (medium !== "msisdn" || mxid !== userId) { + continue; + } + const msisdn = msisdns.find(e => e.address === address); + if (!msisdn) continue; + msisdn.bound = true; + } + } + + this.setState({ msisdns }); + } + + render() { + let content; + if (this.state.msisdns.length > 0) { + content = this.state.msisdns.map((e) => { + return ; + }); + } else { + content = + {_t("Discovery options will appear once you have added a phone number above.")} + ; + } + + return ( +
+ {content} +
+ ); + } +} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 8283d26ef1..fc1a9b8c4a 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -164,6 +164,21 @@ export default class GeneralUserSettingsTab extends React.Component { ); } + _renderDiscoverySection() { + const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses"); + const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers"); + + return ( +
+ {_t("Email addresses")} + + + {_t("Phone numbers")} + +
+ ); + } + _renderManagementSection() { // TODO: Improve warning text for account deactivation return ( @@ -187,6 +202,9 @@ export default class GeneralUserSettingsTab extends React.Component { {this._renderAccountSection()} {this._renderLanguageSection()} {this._renderThemeSection()} +
{_t("Discovery")}
+ {this._renderDiscoverySection()} +
{_t("Deactivate account")}
{this._renderManagementSection()}
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9ad20bf56c..34f11bf2cf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -470,18 +470,6 @@ "Last seen": "Last seen", "Select devices": "Select devices", "Failed to set display name": "Failed to set display name", - "Unable to remove contact information": "Unable to remove contact information", - "Are you sure?": "Are you sure?", - "Yes": "Yes", - "No": "No", - "Remove": "Remove", - "Invalid Email Address": "Invalid Email Address", - "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", - "Unable to add email address": "Unable to add email address", - "Unable to verify email address.": "Unable to verify email address.", - "Add": "Add", - "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.", - "Email Address": "Email Address", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", "No integrations server configured": "No integrations server configured", @@ -541,11 +529,6 @@ "Off": "Off", "On": "On", "Noisy": "Noisy", - "Unable to verify phone number.": "Unable to verify phone number.", - "Incorrect verification code": "Incorrect verification code", - "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", - "Verification code": "Verification code", - "Phone Number": "Phone Number", "Profile picture": "Profile picture", "Upload profile picture": "Upload profile picture", "Upgrade to your own domain": "Upgrade to your own domain", @@ -568,6 +551,8 @@ "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", "General": "General", + "Discovery": "Discovery", + "Deactivate account": "Deactivate account", "Legal": "Legal", "Credits": "Credits", "For help with using Riot, click here.": "For help with using Riot, click here.", @@ -688,6 +673,33 @@ "Encrypted": "Encrypted", "Who can access this room?": "Who can access this room?", "Who can read history?": "Who can read history?", + "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", + "Unable to share email address": "Unable to share email address", + "Unable to verify email address.": "Unable to verify email address.", + "Check your inbox, then click Continue": "Check your inbox, then click Continue", + "Revoke": "Revoke", + "Share": "Share", + "Discovery options will appear once you have added an email above.": "Discovery options will appear once you have added an email above.", + "Unable to revoke sharing for phone number": "Unable to revoke sharing for phone number", + "Unable to share phone number": "Unable to share phone number", + "Unable to verify phone number.": "Unable to verify phone number.", + "Incorrect verification code": "Incorrect verification code", + "Please enter verification code sent via text.": "Please enter verification code sent via text.", + "Verification code": "Verification code", + "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", + "Unable to remove contact information": "Unable to remove contact information", + "Are you sure?": "Are you sure?", + "Yes": "Yes", + "No": "No", + "Remove": "Remove", + "Invalid Email Address": "Invalid Email Address", + "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", + "Unable to add email address": "Unable to add email address", + "Add": "Add", + "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.", + "Email Address": "Email Address", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.", + "Phone Number": "Phone Number", "Cannot add any more widgets": "Cannot add any more widgets", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", "Add a widget": "Add a widget", From 11cf7493d98b0c7780e7b85cca5bdf30705e0fee Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 8 Aug 2019 11:52:21 +0100 Subject: [PATCH 019/413] Disable binding 3PIDs when adding to account Binding 3PIDs on the IS is now handled by a separate step in the Discovery section of settings. Fixes https://github.com/vector-im/riot-web/issues/10510 --- src/components/views/settings/account/EmailAddresses.js | 2 +- src/components/views/settings/account/PhoneNumbers.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js index b610b5ab64..c13d2b4e0f 100644 --- a/src/components/views/settings/account/EmailAddresses.js +++ b/src/components/views/settings/account/EmailAddresses.js @@ -164,7 +164,7 @@ export default class EmailAddresses extends React.Component { const task = new AddThreepid(); this.setState({verifying: true, continueDisabled: true, addTask: task}); - task.addEmailAddress(email, true).then(() => { + task.addEmailAddress(email, false).then(() => { this.setState({continueDisabled: false}); }).catch((err) => { console.error("Unable to add email address " + email + " " + err); diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js index d892f17ff8..236a4e7587 100644 --- a/src/components/views/settings/account/PhoneNumbers.js +++ b/src/components/views/settings/account/PhoneNumbers.js @@ -161,7 +161,7 @@ export default class PhoneNumbers extends React.Component { const task = new AddThreepid(); this.setState({verifying: true, continueDisabled: true, addTask: task}); - task.addMsisdn(phoneCountry, phoneNumber, true).then((response) => { + task.addMsisdn(phoneCountry, phoneNumber, false).then((response) => { this.setState({continueDisabled: false, verifyMsisdn: response.msisdn}); }).catch((err) => { console.error("Unable to add phone number " + phoneNumber + " " + err); From e0fb0de83d375562fce6d7f1459495bef5dc0d85 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 8 Aug 2019 16:01:41 +0100 Subject: [PATCH 020/413] Relock gemini-scrollbar deps --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 549b324de3..f6ae81d6e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6397,7 +6397,7 @@ react-dom@^16.4.2: version "2.1.5" resolved "https://codeload.github.com/matrix-org/react-gemini-scrollbar/tar.gz/f64452388011d37d8a4427ba769153c30700ab8c" dependencies: - gemini-scrollbar matrix-org/gemini-scrollbar#b302279 + gemini-scrollbar matrix-org/gemini-scrollbar#91e1e566 react-immutable-proptypes@^2.1.0: version "2.1.0" From c2effe47c4b3d42a133fc1a514003dde61774cec Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 8 Aug 2019 13:34:14 +0000 Subject: [PATCH 021/413] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1697 of 1697 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 2d1a801fe8..f9c7fec1ba 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2066,5 +2066,14 @@ "Service": "服務", "Summary": "摘要", "Terms": "條款", - "This account has been deactivated.": "此帳號已停用。" + "This account has been deactivated.": "此帳號已停用。", + "Failed to start chat": "啟動聊天失敗", + "Messages": "訊息", + "Actions": "動作", + "Displays list of commands with usages and descriptions": "顯示包含用法與描述的指令清單", + "Always show the window menu bar": "總是顯示視窗選單列", + "Command Help": "指令說明", + "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "未設定身份識別伺服器,所以您無法新增未來可以用於重設您密碼的電子郵件地址。", + "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "未設定身份識別伺服器:無法新增電子郵件地址。您將無法重設您的密碼。", + "No identity server is configured: add one in server settings to reset your password.": "未設定身份識別伺服器:在伺服器設定中新增一個以重設您的密碼。" } From 38e64ce966437d12463276066c63f4e478fe348b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 8 Aug 2019 15:52:54 +0100 Subject: [PATCH 022/413] Remove 3PID binding during registration This disables 3PID binding at registration time, so users won't be discoverable by 3PID by default. Instead, new discovery controls in settings allow you to opt-in. Fixes https://github.com/vector-im/riot-web/issues/10424 --- src/components/structures/auth/Registration.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index fa390ace15..1c094cf862 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -403,14 +403,9 @@ module.exports = React.createClass({ // clicking the email link. let inhibitLogin = Boolean(this.state.formVals.email); - // Only send the bind params if we're sending username / pw params + // Only send inhibitLogin if we're sending username / pw params // (Since we need to send no params at all to use the ones saved in the // session). - const bindThreepids = this.state.formVals.password ? { - email: true, - msisdn: true, - } : {}; - // Likewise inhibitLogin if (!this.state.formVals.password) inhibitLogin = null; return this.state.matrixClient.register( @@ -418,7 +413,7 @@ module.exports = React.createClass({ this.state.formVals.password, undefined, // session id: included in the auth dict already auth, - bindThreepids, + null, null, inhibitLogin, ); From 28b42d512ad49359df2859e3cefa377fd42ec9ef Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 8 Aug 2019 20:07:38 +0100 Subject: [PATCH 023/413] Use the room name rather than sender name for fallback room avatar event Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/messages/RoomAvatarEvent.js | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index d035fc9237..72d0d1926a 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -31,12 +31,21 @@ module.exports = React.createClass({ mxEvent: PropTypes.object.isRequired, }, - onAvatarClick: function(name) { - const httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url); + onAvatarClick: function() { + const cli = MatrixClientPeg.get(); + const ev = this.props.mxEvent; + const httpUrl = cli.mxcUrlToHttp(ev.getContent().url); + + const room = cli.getRoom(this.props.mxEvent.getRoomId()); + const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', { + senderDisplayName: ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(), + roomName: room ? room.name : '', + }); + const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: httpUrl, - name: name, + name: text, }; Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); }, @@ -47,15 +56,11 @@ module.exports = React.createClass({ const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', { - senderDisplayName: senderDisplayName, - roomName: room ? room.name : '', - }); if (!ev.getContent().url || ev.getContent().url.trim().length === 0) { return (
- { _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) } + { _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName}) }
); } @@ -75,8 +80,8 @@ module.exports = React.createClass({ { 'img': () => - + onClick={this.onAvatarClick}> + , }) } From 423a74c99cc94ba8af2cdc2be999925883530310 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 8 Aug 2019 20:21:53 +0100 Subject: [PATCH 024/413] Clean up implementation Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/avatars/BaseAvatar.js | 7 ++++++- .../views/messages/RoomAvatarEvent.js | 19 +++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 80f5c43d0c..8e13f89d2d 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,7 +20,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import AvatarLogic from '../../../Avatar'; -import sdk from '../../../index'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; @@ -121,6 +121,10 @@ module.exports = React.createClass({ ); urls.push(defaultImageUrl); // lowest priority } + + // deduplicate URLs + urls = Array.from(new Set(urls)); + return { imageUrls: urls, defaultImageUrl: defaultImageUrl, @@ -129,6 +133,7 @@ module.exports = React.createClass({ }, onError: function(ev) { + console.log("onError"); const nextIndex = this.state.urlsIndex + 1; if (nextIndex < this.state.imageUrls.length) { // try the next one diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index 72d0d1926a..17460ee183 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -1,5 +1,6 @@ /* Copyright 2017 Vector Creations Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> 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,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import { ContentRepo } from 'matrix-js-sdk'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import Modal from '../../../Modal'; @@ -53,9 +53,7 @@ module.exports = React.createClass({ render: function() { const ev = this.props.mxEvent; const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - - const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); if (!ev.getContent().url || ev.getContent().url.trim().length === 0) { return ( @@ -65,13 +63,10 @@ module.exports = React.createClass({ ); } - const url = ContentRepo.getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - ev.getContent().url, - Math.ceil(14 * window.devicePixelRatio), - Math.ceil(14 * window.devicePixelRatio), - 'crop', - ); + const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); + const oobData = { + avatarUrl: ev.getContent().url, + }; return (
@@ -81,7 +76,7 @@ module.exports = React.createClass({ 'img': () => - + , }) } From a6347af320944bf10236714ba359e95613056afd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 8 Aug 2019 15:59:35 -0600 Subject: [PATCH 025/413] Fix karma complaining about woff2 files --- karma.conf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karma.conf.js b/karma.conf.js index e2728cdc09..d680ade030 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -166,7 +166,7 @@ module.exports = function (config) { ] }, { - test: /\.(gif|png|svg|ttf)$/, + test: /\.(gif|png|svg|ttf|woff2)$/, loader: 'file-loader', }, ], From ac9682a75bd28d540833136713b909020afd83c8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 8 Aug 2019 15:59:49 -0600 Subject: [PATCH 026/413] Actually use argument in karma tests --- karma.conf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karma.conf.js b/karma.conf.js index d680ade030..d55be049bb 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -28,7 +28,7 @@ process.env.PHANTOMJS_BIN = 'node_modules/.bin/phantomjs'; function fileExists(name) { try { - fs.statSync(gsCss); + fs.statSync(name); return true; } catch (e) { return false; From ee3542453e309d91eb4e8f50487e0c1290f599d7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Aug 2019 11:31:04 +0100 Subject: [PATCH 027/413] Fix RoomAvatarEvent historic fallback Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/avatars/BaseAvatar.js | 1 - src/components/views/messages/RoomAvatarEvent.js | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 8e13f89d2d..afc6faa18d 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -133,7 +133,6 @@ module.exports = React.createClass({ }, onError: function(ev) { - console.log("onError"); const nextIndex = this.state.urlsIndex + 1; if (nextIndex < this.state.imageUrls.length) { // try the next one diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index 17460ee183..207a385b92 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -64,8 +64,10 @@ module.exports = React.createClass({ } const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); + // Provide all arguments to RoomAvatar via oobData because the avatar is historic const oobData = { avatarUrl: ev.getContent().url, + name: room ? room.name : "", }; return ( @@ -76,7 +78,7 @@ module.exports = React.createClass({ 'img': () => - + , }) } From 72a83a8e6e077b01218f50485b9c146c4f420d57 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Aug 2019 14:51:07 +0100 Subject: [PATCH 028/413] Add mount-guards to MImageBody Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/MImageBody.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 78af5da727..de19d0026f 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd -Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2018, 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -264,6 +264,7 @@ export default class MImageBody extends React.Component { decryptedBlob = blob; return URL.createObjectURL(blob); }).then((contentUrl) => { + if (this.unmounted) return; this.setState({ decryptedUrl: contentUrl, decryptedThumbnailUrl: thumbnailUrl, @@ -271,6 +272,7 @@ export default class MImageBody extends React.Component { }); }); }).catch((err) => { + if (this.unmounted) return; console.warn("Unable to decrypt attachment: ", err); // Set a placeholder image when we can't decrypt the image. this.setState({ From fcdbce1ddae0a17e1ed021151aa488defe8b8e58 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 9 Aug 2019 15:30:05 +0100 Subject: [PATCH 029/413] Change throttle to debounce And add an explanation of whyI think one's more apropriate than the other for this. --- src/components/views/elements/Field.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 93bea70fc8..e920bdb0fd 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -18,7 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import sdk from '../../../index'; -import { throttle } from 'lodash'; +import { debounce } from 'lodash'; // Invoke validation from user input (when typing, etc.) at most once every N ms. const VALIDATION_THROTTLE_MS = 200; @@ -118,7 +118,16 @@ export default class Field extends React.PureComponent { } } - validateOnChange = throttle(() => { + /* + * This was changed from throttle to debounce: this is more traditional for + * form validation since it means that the validation doesn't happen at all + * until the user stops typing for a bit (debounce defaults to not running on + * the leading edge). If we're doing an HTTP hit on each validation, we have more + * incentive to prevent validating input that's very unlikely to be valid. + * We may find that we actually want different behaviour for registration + * fields, in which case we can add some options to control it. + */ + validateOnChange = debounce(() => { this.validate({ focused: true, }); From 787e9feeff4071be4f3c3d02779ffaf8d2066dc4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Aug 2019 15:34:03 +0100 Subject: [PATCH 030/413] Get rid of support for legacy login params Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Login.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Login.js b/src/Login.js index c31a9308a8..d9ce8adaaa 100644 --- a/src/Login.js +++ b/src/Login.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -87,32 +88,23 @@ export default class Login { const isEmail = username.indexOf("@") > 0; let identifier; - let legacyParams; // parameters added to support old HSes if (phoneCountry && phoneNumber) { identifier = { type: 'm.id.phone', country: phoneCountry, number: phoneNumber, }; - // No legacy support for phone number login } else if (isEmail) { identifier = { type: 'm.id.thirdparty', medium: 'email', address: username, }; - legacyParams = { - medium: 'email', - address: username, - }; } else { identifier = { type: 'm.id.user', user: username, }; - legacyParams = { - user: username, - }; } const loginParams = { @@ -120,7 +112,6 @@ export default class Login { identifier: identifier, initial_device_display_name: this._defaultDeviceDisplayName, }; - Object.assign(loginParams, legacyParams); const tryFallbackHs = (originalError) => { return sendLoginRequest( From 0050ed7e733714a7d969dac88fe8f1557169d104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Fri, 9 Aug 2019 07:30:35 +0000 Subject: [PATCH 031/413] Translated using Weblate (French) Currently translated at 100.0% (1709 of 1709 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index df292e8957..951d8e864b 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2073,5 +2073,27 @@ "Service": "Service", "Summary": "Résumé", "Terms": "Conditions", - "This account has been deactivated.": "Ce compte a été désactivé." + "This account has been deactivated.": "Ce compte a été désactivé.", + "Failed to start chat": "Échec du démarrage de la discussion", + "Messages": "Messages", + "Actions": "Actions", + "Displays list of commands with usages and descriptions": "Affiche la liste des commandes avec leurs utilisations et descriptions", + "Discovery": "Découverte", + "Deactivate account": "Désactiver le compte", + "Always show the window menu bar": "Toujours afficher la barre de menu de la fenêtre", + "Unable to revoke sharing for email address": "Impossible de révoquer le partage pour l’adresse e-mail", + "Unable to share email address": "Impossible de partager l’adresse e-mail", + "Check your inbox, then click Continue": "Vérifiez votre boîte de réception puis cliquez sur Continuer", + "Revoke": "Révoquer", + "Share": "Partager", + "Discovery options will appear once you have added an email above.": "Les options de découverte apparaîtront quand vous aurez ajouté une adresse e-mail ci-dessus.", + "Unable to revoke sharing for phone number": "Impossible de révoquer le partage pour le numéro de téléphone", + "Unable to share phone number": "Impossible de partager le numéro de téléphone", + "Please enter verification code sent via text.": "Veuillez saisir le code de vérification envoyé par SMS.", + "Discovery options will appear once you have added a phone number above.": "Les options de découverte apparaîtront quand vous aurez ajouté un numéro de téléphone ci-dessus.", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Un message textuel a été envoyé à +%(msisdn)s. Saisissez le code de vérification qu’il contient.", + "Command Help": "Aide aux commandes", + "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "Aucun serveur d’identité n’est configuré donc vous ne pouvez pas ajouter d’adresse e-mail pour pouvoir réinitialiser votre mot de passe dans l’avenir.", + "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "Aucun serveur d’identité n’est configuré : aucune adresse e-mail ne peut être ajoutée. Vous ne pourrez pas réinitialiser votre mot de passe.", + "No identity server is configured: add one in server settings to reset your password.": "Aucun serveur d’identité n’est configuré : ajoutez-en un dans les paramètres du serveur pour réinitialiser votre mot de passe." } From 7fe078c07a021ec439f7055df1d5473f23c9d9ae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Aug 2019 16:47:22 +0100 Subject: [PATCH 032/413] Modal.createX return thenable which extends onFinished, for async/await Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Modal.js | 25 +++++++++++++++++-------- src/components/structures/MatrixChat.js | 21 ++++++++++----------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/Modal.js b/src/Modal.js index fd0fdc0501..96be445ab1 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -23,6 +23,7 @@ import Analytics from './Analytics'; import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; +import Promise from "bluebird"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; @@ -182,7 +183,7 @@ class ModalManager { const modal = {}; // never call this from onFinished() otherwise it will loop - const closeDialog = this._getCloseFn(modal, props); + const [closeDialog, onFinishedProm] = this._getCloseFn(modal, props); // don't attempt to reuse the same AsyncWrapper for different dialogs, // otherwise we'll get confused. @@ -197,11 +198,13 @@ class ModalManager { modal.onFinished = props ? props.onFinished : null; modal.className = className; - return {modal, closeDialog}; + return {modal, closeDialog, onFinishedProm}; } _getCloseFn(modal, props) { - return (...args) => { + const deferred = Promise.defer(); + return [(...args) => { + deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); const i = this._modals.indexOf(modal); if (i >= 0) { @@ -223,7 +226,7 @@ class ModalManager { } this._reRender(); - }; + }, deferred.promise]; } /** @@ -256,7 +259,7 @@ class ModalManager { * @returns {object} Object with 'close' parameter being a function that will close the dialog */ createDialogAsync(prom, props, className, isPriorityModal, isStaticModal) { - const {modal, closeDialog} = this._buildModal(prom, props, className); + const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className); if (isPriorityModal) { // XXX: This is destructive @@ -269,15 +272,21 @@ class ModalManager { } this._reRender(); - return {close: closeDialog}; + return { + close: closeDialog, + then: (resolve, reject) => onFinishedProm.then(resolve, reject), + }; } appendDialogAsync(prom, props, className) { - const {modal, closeDialog} = this._buildModal(prom, props, className); + const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className); this._modals.push(modal); this._reRender(); - return {close: closeDialog}; + return { + close: closeDialog, + then: (resolve, reject) => onFinishedProm.then(resolve, reject), + }; } closeAll() { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index bcc0923f14..deef8488f4 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -931,18 +931,17 @@ export default React.createClass({ }).close; }, - _createRoom: function() { + _createRoom: async function() { const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); - Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { - onFinished: (shouldCreate, name, noFederate) => { - if (shouldCreate) { - const createOpts = {}; - if (name) createOpts.name = name; - if (noFederate) createOpts.creation_content = {'m.federate': false}; - createRoom({createOpts}).done(); - } - }, - }); + const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog); + + const [shouldCreate, name, noFederate] = await modal; + if (shouldCreate) { + const createOpts = {}; + if (name) createOpts.name = name; + if (noFederate) createOpts.creation_content = {'m.federate': false}; + createRoom({createOpts}).done(); + } }, _chatCreateOrReuse: function(userId) { From 49c77305035c277b4cf03fff8214c7eda0b722bf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Aug 2019 17:29:22 +0100 Subject: [PATCH 033/413] change Modal async/await signature to use raw promises Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Modal.js | 4 ++-- src/components/structures/MatrixChat.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Modal.js b/src/Modal.js index 96be445ab1..26c9da8bbb 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -274,7 +274,7 @@ class ModalManager { this._reRender(); return { close: closeDialog, - then: (resolve, reject) => onFinishedProm.then(resolve, reject), + finished: onFinishedProm, }; } @@ -285,7 +285,7 @@ class ModalManager { this._reRender(); return { close: closeDialog, - then: (resolve, reject) => onFinishedProm.then(resolve, reject), + finished: onFinishedProm, }; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index deef8488f4..b8903076c7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -935,7 +935,7 @@ export default React.createClass({ const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog); - const [shouldCreate, name, noFederate] = await modal; + const [shouldCreate, name, noFederate] = await modal.finished; if (shouldCreate) { const createOpts = {}; if (name) createOpts.name = name; From c76514fceb78b91688fd7fee4345a25f9490a225 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 9 Aug 2019 18:07:58 +0100 Subject: [PATCH 034/413] Add UI in settings to change ID Server Just changes the current ID server being used To come in subsequent PRs: * Store this in account data * Check for terms or support the proper UI for accepting terms when setting * Support disconnecting Part 1 of https://github.com/vector-im/riot-web/issues/10094 Requires https://github.com/matrix-org/matrix-js-sdk/pull/1013 --- res/css/_components.scss | 1 + res/css/views/elements/_Tooltip.scss | 2 +- src/components/views/elements/Field.js | 15 +++++++++------ .../settings/tabs/user/GeneralUserSettingsTab.js | 13 ++++++++----- src/i18n/strings/en_EN.json | 13 +++++++++++-- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/res/css/_components.scss b/res/css/_components.scss index dff174e943..abfce47916 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -168,6 +168,7 @@ @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_ProfileSettings.scss"; +@import "./views/settings/_SetIdServer.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss"; diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 8f6204c942..cc4eb409df 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -55,7 +55,7 @@ limitations under the License. border-radius: 4px; box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; background-color: $menu-bg-color; - z-index: 2000; + z-index: 4000; // Higher than dialogs so tooltips can be used in dialogs padding: 10px; pointer-events: none; line-height: 14px; diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index e920bdb0fd..554d5d6181 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -46,6 +46,9 @@ export default class Field extends React.PureComponent { // and a `feedback` react component field to provide feedback // to the user. onValidate: PropTypes.func, + // If specified, contents will appear as a tooltip on the element and + // validation feedback tooltips will be suppressed. + tooltip: PropTypes.node, // All other props pass through to the . }; @@ -134,7 +137,7 @@ export default class Field extends React.PureComponent { }, VALIDATION_THROTTLE_MS); render() { - const { element, prefix, onValidate, children, ...inputProps } = this.props; + const { element, prefix, onValidate, children, tooltip, ...inputProps } = this.props; const inputElement = element || "input"; @@ -165,12 +168,12 @@ export default class Field extends React.PureComponent { // Handle displaying feedback on validity const Tooltip = sdk.getComponent("elements.Tooltip"); - let tooltip; - if (this.state.feedback) { - tooltip = ; } @@ -178,7 +181,7 @@ export default class Field extends React.PureComponent { {prefixContainer} {fieldInput} - {tooltip} + {fieldTooltip}
; } } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index a9c010b6b4..4c0ebef3f3 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -26,11 +26,11 @@ import LanguageDropdown from "../../../elements/LanguageDropdown"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; -const PlatformPeg = require("../../../../../PlatformPeg"); -const MatrixClientPeg = require("../../../../../MatrixClientPeg"); -const sdk = require('../../../../..'); -const Modal = require("../../../../../Modal"); -const dis = require("../../../../../dispatcher"); +import PlatformPeg from "../../../../../PlatformPeg"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; +import sdk from "../../../../.."; +import Modal from "../../../../../Modal"; +import dis from "../../../../../dispatcher"; export default class GeneralUserSettingsTab extends React.Component { static propTypes = { @@ -171,6 +171,7 @@ export default class GeneralUserSettingsTab extends React.Component { _renderDiscoverySection() { const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses"); const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers"); + const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); return (
@@ -179,6 +180,8 @@ export default class GeneralUserSettingsTab extends React.Component { {_t("Phone numbers")} + { /* has its own heading as it includes the current ID server */ } +
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8173051c30..1d7051e361 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -537,6 +537,17 @@ "Upgrade to your own domain": "Upgrade to your own domain", "Display Name": "Display Name", "Save": "Save", + "Identity Server URL must be HTTPS": "Identity Server URL must be HTTPS", + "Could not connect to ID Server": "Could not connect to ID Server", + "Not a valid ID Server (status code %(code)s)": "Not a valid ID Server (status code %(code)s)", + "Identity Server": "Identity Server", + "Enter the URL of the Identity Server to use": "Enter the URL of the Identity Server to use", + "Looks good": "Looks good", + "Checking Server": "Checking Server", + "Identity Server (%(server)s)": "Identity Server (%(server)s)", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", + "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below": "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below", + "Change": "Change", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", @@ -1276,7 +1287,6 @@ "Missing session data": "Missing session data", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.", - "Identity Server": "Identity Server", "Integrations Manager": "Integrations Manager", "Find others by phone or email": "Find others by phone or email", "Be found by phone or email": "Be found by phone or email", @@ -1396,7 +1406,6 @@ "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", "Sign in to your Matrix account on ": "Sign in to your Matrix account on ", - "Change": "Change", "Sign in with": "Sign in with", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.", From 242f23c7531faf3d42faad567fca7b9ce8057484 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Aug 2019 18:08:17 +0100 Subject: [PATCH 035/413] RegistrationForm: the Fields are controlled, fix default values Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/RegistrationForm.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index cd3dab12ac..f3b9640e16 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018, 2019 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -69,10 +70,10 @@ module.exports = React.createClass({ fieldValid: {}, // The ISO2 country code selected in the phone number entry phoneCountry: this.props.defaultPhoneCountry, - username: "", - email: "", - phoneNumber: "", - password: "", + username: this.props.defaultUsername || "", + email: this.props.defaultEmail || "", + phoneNumber: this.props.defaultPhoneNumber || "", + password: this.props.defaultPassword || "", passwordConfirm: "", passwordComplexity: null, passwordSafe: false, @@ -90,7 +91,7 @@ module.exports = React.createClass({ } const self = this; - if (this.state.email == '') { + if (this.state.email === '') { const haveIs = Boolean(this.props.serverConfig.isUrl); let desc; @@ -455,7 +456,6 @@ module.exports = React.createClass({ ref={field => this[FIELD_EMAIL] = field} type="text" label={emailPlaceholder} - defaultValue={this.props.defaultEmail} value={this.state.email} onChange={this.onEmailChange} onValidate={this.onEmailValidate} @@ -469,7 +469,6 @@ module.exports = React.createClass({ ref={field => this[FIELD_PASSWORD] = field} type="password" label={_t("Password")} - defaultValue={this.props.defaultPassword} value={this.state.password} onChange={this.onPasswordChange} onValidate={this.onPasswordValidate} @@ -483,7 +482,6 @@ module.exports = React.createClass({ ref={field => this[FIELD_PASSWORD_CONFIRM] = field} type="password" label={_t("Confirm")} - defaultValue={this.props.defaultPassword} value={this.state.passwordConfirm} onChange={this.onPasswordConfirmChange} onValidate={this.onPasswordConfirmValidate} @@ -512,7 +510,6 @@ module.exports = React.createClass({ ref={field => this[FIELD_PHONE_NUMBER] = field} type="text" label={phoneLabel} - defaultValue={this.props.defaultPhoneNumber} value={this.state.phoneNumber} prefix={phoneCountry} onChange={this.onPhoneNumberChange} @@ -528,7 +525,6 @@ module.exports = React.createClass({ type="text" autoFocus={true} label={_t("Username")} - defaultValue={this.props.defaultUsername} value={this.state.username} onChange={this.onUsernameChange} onValidate={this.onUsernameValidate} From 4075cdde7fd094f14996cd9abb3d157bb6761f19 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 9 Aug 2019 18:59:57 +0100 Subject: [PATCH 036/413] lint --- src/components/views/elements/Field.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 554d5d6181..b432bd0b8f 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -138,6 +138,7 @@ export default class Field extends React.PureComponent { render() { const { element, prefix, onValidate, children, tooltip, ...inputProps } = this.props; + !tooltip; // needs to be removed from props but we don't need it here, so otherwise unused variable const inputElement = element || "input"; From ffa49df8892fa5377312dce28adb721326a9009e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Aug 2019 16:05:05 -0600 Subject: [PATCH 037/413] Refactor integration manager handling into a common place It was already in a common place, but this is the boilerplate for supporting multiple integration managers, and multiple integration manager sources. For https://github.com/vector-im/riot-web/issues/4913 / https://github.com/vector-im/riot-web/issues/10161 --- src/CallHandler.js | 22 +++-- src/FromWidgetPostMessageApi.js | 13 +-- src/ScalarAuthClient.js | 69 ++++++++++------ src/components/views/elements/AppTile.js | 31 ++++--- .../views/elements/ManageIntegsButton.js | 12 ++- src/components/views/messages/TextualBody.js | 13 ++- src/components/views/rooms/AppsDrawer.js | 7 +- src/components/views/rooms/Stickerpicker.js | 19 ++--- .../IntegrationManagerInstance.js | 81 +++++++++++++++++++ src/integrations/IntegrationManagers.js | 68 ++++++++++++++++ src/integrations/integrations.js | 79 ------------------ 11 files changed, 267 insertions(+), 147 deletions(-) create mode 100644 src/integrations/IntegrationManagerInstance.js create mode 100644 src/integrations/IntegrationManagers.js delete mode 100644 src/integrations/integrations.js diff --git a/src/CallHandler.js b/src/CallHandler.js index 5b58400ae6..8dfd283e60 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -63,7 +63,7 @@ import SdkConfig from './SdkConfig'; import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; -import ScalarAuthClient from './ScalarAuthClient'; +import {IntegrationManagers} from "./integrations/IntegrationManagers"; global.mxCalls = { //room_id: MatrixCall @@ -348,14 +348,20 @@ async function _startCallApp(roomId, type) { // the state event in anyway, but the resulting widget would then not // work for us. Better that the user knows before everyone else in the // room sees it. - const scalarClient = new ScalarAuthClient(); - let haveScalar = false; - try { - await scalarClient.connect(); - haveScalar = scalarClient.hasCredentials(); - } catch (e) { - // fall through + const managers = IntegrationManagers.sharedInstance(); + let haveScalar = true; + if (managers.hasManager()) { + try { + const scalarClient = managers.getPrimaryManager().getScalarClient(); + await scalarClient.connect(); + haveScalar = scalarClient.hasCredentials(); + } catch (e) { + // ignore + } + } else { + haveScalar = false; } + if (!haveScalar) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index d34e3d8ed0..b2bd579b74 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -22,7 +22,7 @@ import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import MatrixClientPeg from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; -import { showIntegrationsManager } from './integrations/integrations'; +import {IntegrationManagers} from "./integrations/IntegrationManagers"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -193,11 +193,12 @@ export default class FromWidgetPostMessageApi { const integType = (data && data.integType) ? data.integType : null; const integId = (data && data.integId) ? data.integId : null; - showIntegrationsManager({ - room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), - screen: 'type_' + integType, - integrationId: integId, - }); + // TODO: Open the right integration manager for the widget + IntegrationManagers.sharedInstance().getPrimaryManager().open( + MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), + `type_${integType}`, + integId, + ); } else if (action === 'set_always_on_screen') { // This is a new message: there is no reason to support the deprecated widgetData here const data = event.data.data; diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index c268fbe3fb..3623d47f8e 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -29,20 +29,43 @@ import * as Matrix from 'matrix-js-sdk'; // The version of the integration manager API we're intending to work with const imApiVersion = "1.1"; -class ScalarAuthClient { - constructor() { +export default class ScalarAuthClient { + constructor(apiUrl, uiUrl) { + this.apiUrl = apiUrl; + this.uiUrl = uiUrl; this.scalarToken = null; // `undefined` to allow `startTermsFlow` to fallback to a default // callback if this is unset. this.termsInteractionCallback = undefined; + + // We try and store the token on a per-manager basis, but need a fallback + // for the default manager. + const configApiUrl = SdkConfig.get()['integrations_rest_url']; + const configUiUrl = SdkConfig.get()['integrations_ui_url']; + this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl; } - /** - * Determines if setting up a ScalarAuthClient is even possible - * @returns {boolean} true if possible, false otherwise. - */ - static isPossible() { - return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url']; + _writeTokenToStore() { + window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); + if (this.isDefaultManager) { + // We remove the old token from storage to migrate upwards. This is safe + // to do because even if the user switches to /app when this is on /develop + // they'll at worst register for a new token. + window.localStorage.removeItem("mx_scalar_token"); // no-op when not present + } + } + + _readTokenFromStore() { + let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); + if (!token && this.isDefaultManager) { + token = window.localStorage.getItem("mx_scalar_token"); + } + return token; + } + + _readToken() { + if (this.scalarToken) return this.scalarToken; + return this._readTokenFromStore(); } setTermsInteractionCallback(callback) { @@ -61,8 +84,7 @@ class ScalarAuthClient { // Returns a promise that resolves to a scalar_token string getScalarToken() { - let token = this.scalarToken; - if (!token) token = window.localStorage.getItem("mx_scalar_token"); + const token = this._readToken(); if (!token) { return this.registerForToken(); @@ -78,7 +100,7 @@ class ScalarAuthClient { } _getAccountName(token) { - const url = SdkConfig.get().integrations_rest_url + "/account"; + const url = this.apiUrl + "/account"; return new Promise(function(resolve, reject) { request({ @@ -111,7 +133,7 @@ class ScalarAuthClient { return token; }).catch((e) => { if (e instanceof TermsNotSignedError) { - console.log("Integrations manager requires new terms to be agreed to"); + console.log("Integration manager requires new terms to be agreed to"); // The terms endpoints are new and so live on standard _matrix prefixes, // but IM rest urls are currently configured with paths, so remove the // path from the base URL before passing it to the js-sdk @@ -126,7 +148,7 @@ class ScalarAuthClient { // Once we've fully transitioned to _matrix URLs, we can give people // a grace period to update their configs, then use the rest url as // a regular base url. - const parsedImRestUrl = url.parse(SdkConfig.get().integrations_rest_url); + const parsedImRestUrl = url.parse(this.apiUrl); parsedImRestUrl.path = ''; parsedImRestUrl.pathname = ''; return startTermsFlow([new Service( @@ -147,17 +169,18 @@ class ScalarAuthClient { return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { // Now we can send that to scalar and exchange it for a scalar token return this.exchangeForScalarToken(tokenObject); - }).then((tokenObject) => { + }).then((token) => { // Validate it (this mostly checks to see if the IM needs us to agree to some terms) - return this._checkToken(tokenObject); - }).then((tokenObject) => { - window.localStorage.setItem("mx_scalar_token", tokenObject); - return tokenObject; + return this._checkToken(token); + }).then((token) => { + this.scalarToken = token; + this._writeTokenToStore(); + return token; }); } exchangeForScalarToken(openidTokenObject) { - const scalarRestUrl = SdkConfig.get().integrations_rest_url; + const scalarRestUrl = this.apiUrl; return new Promise(function(resolve, reject) { request({ @@ -181,7 +204,7 @@ class ScalarAuthClient { } getScalarPageTitle(url) { - let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup'; + let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup'; scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); @@ -217,7 +240,7 @@ class ScalarAuthClient { * @return {Promise} Resolves on completion */ disableWidgetAssets(widgetType, widgetId) { - let url = SdkConfig.get().integrations_rest_url + '/widgets/set_assets_state'; + let url = this.apiUrl + '/widgets/set_assets_state'; url = this.getStarterLink(url); return new Promise((resolve, reject) => { request({ @@ -246,7 +269,7 @@ class ScalarAuthClient { getScalarInterfaceUrlForRoom(room, screen, id) { const roomId = room.roomId; const roomName = room.name; - let url = SdkConfig.get().integrations_ui_url; + let url = this.uiUrl; url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "&room_id=" + encodeURIComponent(roomId); url += "&room_name=" + encodeURIComponent(roomName); @@ -264,5 +287,3 @@ class ScalarAuthClient { return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); } } - -module.exports = ScalarAuthClient; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 38830d78f2..82953cf52e 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,7 +22,6 @@ import qs from 'querystring'; import React from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import ScalarAuthClient from '../../../ScalarAuthClient'; import WidgetMessaging from '../../../WidgetMessaging'; import AccessibleButton from './AccessibleButton'; import Modal from '../../../Modal'; @@ -35,7 +34,7 @@ import WidgetUtils from '../../../utils/WidgetUtils'; import dis from '../../../dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; -import { showIntegrationsManager } from '../../../integrations/integrations'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -178,9 +177,22 @@ export default class AppTile extends React.Component { return; } + const managers = IntegrationManagers.sharedInstance(); + if (!managers.hasManager()) { + console.warn("No integration manager - not setting scalar token", url); + this.setState({ + error: null, + widgetUrl: this._addWurlParams(this.props.url), + initialising: false, + }); + return; + } + + // TODO: Pick the right manager for the widget + // Fetch the token before loading the iframe as we need it to mangle the URL if (!this._scalarClient) { - this._scalarClient = new ScalarAuthClient(); + this._scalarClient = managers.getPrimaryManager().getScalarClient(); } this._scalarClient.getScalarToken().done((token) => { // Append scalar_token as a query param if not already present @@ -189,7 +201,7 @@ export default class AppTile extends React.Component { const params = qs.parse(u.query); if (!params.scalar_token) { params.scalar_token = encodeURIComponent(token); - // u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options + // u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options u.search = undefined; u.query = params; } @@ -251,11 +263,12 @@ export default class AppTile extends React.Component { if (this.props.onEditClick) { this.props.onEditClick(); } else { - showIntegrationsManager({ - room: this.props.room, - screen: 'type_' + this.props.type, - integrationId: this.props.id, - }); + // TODO: Open the right manager for the widget + IntegrationManagers.sharedInstance().getPrimaryManager().open( + this.props.room, + this.props.type, + this.props.id, + ); } } diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index f5b6d75d6c..ca7391329f 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -18,9 +18,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import ScalarAuthClient from '../../../ScalarAuthClient'; import { _t } from '../../../languageHandler'; -import { showIntegrationsManager } from '../../../integrations/integrations'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; export default class ManageIntegsButton extends React.Component { constructor(props) { @@ -30,12 +29,17 @@ export default class ManageIntegsButton extends React.Component { onManageIntegrations = (ev) => { ev.preventDefault(); - showIntegrationsManager({ room: this.props.room }); + const managers = IntegrationManagers.sharedInstance(); + if (!managers.hasManager()) { + managers.openNoManagerDialog(); + } else { + managers.getPrimaryManager().open(this.props.room); + } }; render() { let integrationsButton =
; - if (ScalarAuthClient.isPossible()) { + if (IntegrationManagers.sharedInstance().hasManager()) { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); integrationsButton = ( { const completeUrl = scalarClient.getStarterLink(starterLink); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const integrationsUrl = SdkConfig.get().integrations_ui_url; + const integrationsUrl = integrationManager.uiUrl; Modal.createTrackedDialog('Add an integration', '', QuestionDialog, { title: _t("Add an Integration"), description: diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 2e9d3e5071..4d2c1e0380 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -29,7 +29,7 @@ import { _t } from '../../../languageHandler'; import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetEchoStore from "../../../stores/WidgetEchoStore"; import AccessibleButton from '../elements/AccessibleButton'; -import { showIntegrationsManager } from '../../../integrations/integrations'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; // The maximum number of widgets that can be added in a room const MAX_WIDGETS = 2; @@ -128,10 +128,7 @@ module.exports = React.createClass({ }, _launchManageIntegrations: function() { - showIntegrationsManager({ - room: this.props.room, - screen: 'add_integ', - }); + IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ'); }, onClickAddWidget: function(e) { diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 6c48351992..2d3508c404 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -18,13 +18,12 @@ import {_t, _td} from '../../../languageHandler'; import AppTile from '../elements/AppTile'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; -import ScalarAuthClient from '../../../ScalarAuthClient'; import dis from '../../../dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; import WidgetUtils from '../../../utils/WidgetUtils'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import PersistedElement from "../elements/PersistedElement"; -import { showIntegrationsManager } from '../../../integrations/integrations'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; const widgetType = 'm.stickerpicker'; @@ -67,8 +66,9 @@ export default class Stickerpicker extends React.Component { _acquireScalarClient() { if (this.scalarClient) return Promise.resolve(this.scalarClient); - if (ScalarAuthClient.isPossible()) { - this.scalarClient = new ScalarAuthClient(); + // TODO: Pick the right manager for the widget + if (IntegrationManagers.sharedInstance().hasManager()) { + this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient(); return this.scalarClient.connect().then(() => { this.forceUpdate(); return this.scalarClient; @@ -348,11 +348,12 @@ export default class Stickerpicker extends React.Component { * Launch the integrations manager on the stickers integration page */ _launchManageIntegrations() { - showIntegrationsManager({ - room: this.props.room, - screen: `type_${widgetType}`, - integrationId: this.state.widgetId, - }); + // TODO: Open the right integration manager for the widget + IntegrationManagers.sharedInstance().getPrimaryManager().open( + this.props.room, + `type_${widgetType}`, + this.state.widgetId, + ); } render() { diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js new file mode 100644 index 0000000000..b5f6e4f2a8 --- /dev/null +++ b/src/integrations/IntegrationManagerInstance.js @@ -0,0 +1,81 @@ +/* +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. +*/ + +import ScalarAuthClient from "../ScalarAuthClient"; +import sdk from "../index"; +import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms"; +import type {Room} from "matrix-js-sdk"; +import Modal from '../Modal'; + +export class IntegrationManagerInstance { + apiUrl: string; + uiUrl: string; + + constructor(apiUrl: string, uiUrl: string) { + this.apiUrl = apiUrl; + this.uiUrl = uiUrl; + + // Per the spec: UI URL is optional. + if (!this.uiUrl) this.uiUrl = this.apiUrl; + } + + getScalarClient(): ScalarAuthClient { + return new ScalarAuthClient(this.apiUrl, this.uiUrl); + } + + async open(room: Room = null, screen: string = null, integrationId: string = null): void { + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const dialog = Modal.createTrackedDialog( + 'Integration Manager', '', IntegrationsManager, + {loading: true}, 'mx_IntegrationsManager', + ); + + const client = this.getScalarClient(); + client.setTermsInteractionCallback((policyInfo, agreedUrls) => { + // To avoid visual glitching of two modals stacking briefly, we customise the + // terms dialog sizing when it will appear for the integrations manager so that + // it gets the same basic size as the IM's own modal. + return dialogTermsInteractionCallback( + policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager', + ); + }); + + let newProps = {}; + try { + await client.connect(); + if (!client.hasCredentials()) { + newProps["connected"] = false; + } else { + newProps["url"] = client.getScalarInterfaceUrlForRoom(room, screen, integrationId); + } + } catch (e) { + if (e instanceof TermsNotSignedError) { + dialog.close(); + return; + } + + console.error(e); + props["connected"] = false; + } + + // Close the old dialog and open a new one + dialog.close(); + Modal.createTrackedDialog( + 'Integration Manager', '', IntegrationsManager, + newProps, 'mx_IntegrationsManager', + ); + } +} \ No newline at end of file diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js new file mode 100644 index 0000000000..9df5d80ee1 --- /dev/null +++ b/src/integrations/IntegrationManagers.js @@ -0,0 +1,68 @@ +/* +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. +*/ + +import SdkConfig from '../SdkConfig'; +import sdk from "../index"; +import Modal from '../Modal'; +import {IntegrationManagerInstance} from "./IntegrationManagerInstance"; + +export class IntegrationManagers { + static _instance; + + static sharedInstance(): IntegrationManagers { + if (!IntegrationManagers._instance) { + IntegrationManagers._instance = new IntegrationManagers(); + } + return IntegrationManagers._instance; + } + + _managers: IntegrationManagerInstance[] = []; + + constructor() { + this._setupConfiguredManager(); + } + + _setupConfiguredManager() { + const apiUrl = SdkConfig.get()['integrations_rest_url']; + const uiUrl = SdkConfig.get()['integrations_ui_url']; + + if (apiUrl && uiUrl) { + this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl)); + } + } + + hasManager(): boolean { + return this._managers.length > 0; + } + + getPrimaryManager(): IntegrationManagerInstance { + if (this.hasManager()) { + // TODO: TravisR - Handle custom integration managers (widgets) + return this._managers[0]; + } else { + return null; + } + } + + openNoManagerDialog(): void { + // TODO: Is it Integrations (plural) or Integration (singular). Singular is easier spoken. + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + Modal.createTrackedDialog( + "Integration Manager", "None", IntegrationsManager, + {configured: false}, 'mx_IntegrationsManager', + ); + } +} diff --git a/src/integrations/integrations.js b/src/integrations/integrations.js deleted file mode 100644 index dad6cbf3e8..0000000000 --- a/src/integrations/integrations.js +++ /dev/null @@ -1,79 +0,0 @@ -/* -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. -*/ - -import sdk from "../index"; -import ScalarAuthClient from '../ScalarAuthClient'; -import Modal from '../Modal'; -import { TermsNotSignedError, dialogTermsInteractionCallback } from '../Terms'; - -export async function showIntegrationsManager(opts) { - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - - let props = {}; - if (ScalarAuthClient.isPossible()) { - props.loading = true; - } else { - props.configured = false; - } - - const close = Modal.createTrackedDialog( - 'Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager", - ).close; - - if (!ScalarAuthClient.isPossible()) { - return; - } - - const scalarClient = new ScalarAuthClient(); - scalarClient.setTermsInteractionCallback(integrationsTermsInteractionCallback); - try { - await scalarClient.connect(); - if (!scalarClient.hasCredentials()) { - props = { connected: false }; - } else { - props = { - url: scalarClient.getScalarInterfaceUrlForRoom( - opts.room, - opts.screen, - opts.integrationId, - ), - }; - } - } catch (err) { - if (err instanceof TermsNotSignedError) { - // user canceled terms dialog, so just cancel the action - close(); - return; - } - console.error(err); - props = { connected: false }; - } - close(); - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager"); -} - -/* - * To avoid visual glitching of two modals stacking briefly, we customise the - * terms dialog sizing when it will appear for the integrations manager so that - * it gets the same basic size as the IM's own modal. - */ -function integrationsTermsInteractionCallback(policiesAndServicePairs, agreedUrls) { - return dialogTermsInteractionCallback( - policiesAndServicePairs, - agreedUrls, - "mx_TermsDialog_forIntegrationsManager", - ); -} From 5a15e78fe46bd05d11f10ba1798030f03effb6fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Aug 2019 16:14:36 -0600 Subject: [PATCH 038/413] Appease the linter --- src/integrations/IntegrationManagerInstance.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index b5f6e4f2a8..4d0181f017 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -53,7 +53,7 @@ export class IntegrationManagerInstance { ); }); - let newProps = {}; + const newProps = {}; try { await client.connect(); if (!client.hasCredentials()) { @@ -68,7 +68,7 @@ export class IntegrationManagerInstance { } console.error(e); - props["connected"] = false; + newProps["connected"] = false; } // Close the old dialog and open a new one @@ -78,4 +78,4 @@ export class IntegrationManagerInstance { newProps, 'mx_IntegrationsManager', ); } -} \ No newline at end of file +} From 018b4f5d41d4b5c324d9b7fc5de9f41b191a7805 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Aug 2019 17:08:26 -0600 Subject: [PATCH 039/413] Use the default integration manager for config options --- src/CallHandler.js | 3 ++- src/ScalarMessaging.js | 4 +++- src/utils/WidgetUtils.js | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 8dfd283e60..40a8d426f8 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -427,7 +427,8 @@ async function _startCallApp(roomId, type) { // URL, but this will at least allow the integration manager to not be hardcoded. widgetUrl = SdkConfig.get().integrations_jitsi_widget_url + '?' + queryString; } else { - widgetUrl = SdkConfig.get().integrations_rest_url + '/widgets/jitsi.html?' + queryString; + const apiUrl = IntegrationManagers.sharedInstance().getPrimaryManager().apiUrl; + widgetUrl = apiUrl + '/widgets/jitsi.html?' + queryString; } const widgetData = { widgetSessionId }; diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 8b87650929..5d3b3ae506 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -239,6 +239,7 @@ import dis from './dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import RoomViewStore from './stores/RoomViewStore'; import { _t } from './languageHandler'; +import {IntegrationManagers} from "./integrations/IntegrationManagers"; function sendResponse(event, res) { const data = JSON.parse(JSON.stringify(event.data)); @@ -548,7 +549,8 @@ const onMessage = function(event) { // (See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) let configUrl; try { - configUrl = new URL(SdkConfig.get().integrations_ui_url); + // TODO: Support multiple integration managers + configUrl = new URL(IntegrationManagers.sharedInstance().getPrimaryManager().uiUrl); } catch (e) { // No integrations UI URL, ignore silently. return; diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 41a241c905..5e127e48d5 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -27,6 +27,7 @@ import WidgetEchoStore from '../stores/WidgetEchoStore'; const WIDGET_WAIT_TIME = 20000; import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; +import {IntegrationManagers} from "../integrations/IntegrationManagers"; /** * Encodes a URI according to a set of template variables. Variables will be @@ -102,7 +103,8 @@ export default class WidgetUtils { let scalarUrls = SdkConfig.get().integrations_widgets_urls; if (!scalarUrls || scalarUrls.length === 0) { - scalarUrls = [SdkConfig.get().integrations_rest_url]; + const defaultManager = IntegrationManagers.sharedInstance().getPrimaryManager(); + if (defaultManager) scalarUrls = [defaultManager.apiUrl]; } for (let i = 0; i < scalarUrls.length; i++) { From 3a4c6f3eac97ea16d0cafe479384630423e17df1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Aug 2019 17:20:37 -0600 Subject: [PATCH 040/413] Appease the linter again --- src/ScalarMessaging.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 5d3b3ae506..0d61755519 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -232,7 +232,6 @@ Example: } */ -import SdkConfig from './SdkConfig'; import MatrixClientPeg from './MatrixClientPeg'; import { MatrixEvent } from 'matrix-js-sdk'; import dis from './dispatcher'; From 74ce5c3541fdd355ea8f6a3bb01a3cc7a49b8e36 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Aug 2019 17:35:59 -0600 Subject: [PATCH 041/413] Read integration managers from account data For https://github.com/vector-im/riot-web/issues/4913 / https://github.com/vector-im/riot-web/issues/10161 Relies on the structure defined by [MSC1957](https://github.com/matrix-org/matrix-doc/pull/1957) This is just the bit of code to parse the user's widgets (while watching for changes) and allow for it to be the default manager. --- src/Lifecycle.js | 3 ++ src/integrations/IntegrationManagers.js | 48 +++++++++++++++++++++++-- src/utils/WidgetUtils.js | 11 ++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 0ddb7e9aae..c03a958840 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -35,6 +35,7 @@ import { sendLoginRequest } from "./Login"; import * as StorageManager from './utils/StorageManager'; import SettingsStore from "./settings/SettingsStore"; import TypingStore from "./stores/TypingStore"; +import {IntegrationManagers} from "./integrations/IntegrationManagers"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -580,6 +581,7 @@ async function startMatrixClient(startSyncing=true) { Presence.start(); } DMRoomMap.makeShared().start(); + IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.start(); if (startSyncing) { @@ -638,6 +640,7 @@ export function stopMatrixClient(unsetClient=true) { TypingStore.sharedInstance().reset(); Presence.stop(); ActiveWidgetStore.stop(); + IntegrationManagers.sharedInstance().stopWatching(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); const cli = MatrixClientPeg.get(); if (cli) { diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 9df5d80ee1..573d251a7b 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -18,6 +18,9 @@ import SdkConfig from '../SdkConfig'; import sdk from "../index"; import Modal from '../Modal'; import {IntegrationManagerInstance} from "./IntegrationManagerInstance"; +import type {MatrixClient, MatrixEvent} from "matrix-js-sdk"; +import WidgetUtils from "../utils/WidgetUtils"; +import MatrixClientPeg from "../MatrixClientPeg"; export class IntegrationManagers { static _instance; @@ -30,9 +33,28 @@ export class IntegrationManagers { } _managers: IntegrationManagerInstance[] = []; + _client: MatrixClient; constructor() { + this._compileManagers(); + } + + startWatching(): void { + this.stopWatching(); + this._client = MatrixClientPeg.get(); + this._client.on("accountData", this._onAccountData.bind(this)); + this._compileManagers(); + } + + stopWatching(): void { + if (!this._client) return; + this._client.removeListener("accountData", this._onAccountData.bind(this)); + } + + _compileManagers() { + this._managers = []; this._setupConfiguredManager(); + this._setupAccountManagers(); } _setupConfiguredManager() { @@ -44,14 +66,33 @@ export class IntegrationManagers { } } + _setupAccountManagers() { + const widgets = WidgetUtils.getIntegrationManagerWidgets(); + widgets.forEach(w => { + const data = w.content['data']; + if (!data) return; + + const uiUrl = w.content['url']; + const apiUrl = data['api_url']; + if (!apiUrl || !uiUrl) return; + + this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl)); + }); + } + + _onAccountData(ev: MatrixEvent): void { + if (ev.getType() === 'm.widgets') { + this._compileManagers(); + } + } + hasManager(): boolean { return this._managers.length > 0; } getPrimaryManager(): IntegrationManagerInstance { if (this.hasManager()) { - // TODO: TravisR - Handle custom integration managers (widgets) - return this._managers[0]; + return this._managers[this._managers.length - 1]; } else { return null; } @@ -66,3 +107,6 @@ export class IntegrationManagers { ); } } + +// For debugging +global.mxIntegrationManagers = IntegrationManagers; diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 5e127e48d5..1e47554914 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -340,6 +340,17 @@ export default class WidgetUtils { return widgets.filter((widget) => widget.content && widget.content.type === "m.stickerpicker"); } + /** + * Get all integration manager widgets for this user. + * @returns {Object[]} An array of integration manager user widgets. + */ + static getIntegrationManagerWidgets() { + const widgets = WidgetUtils.getUserWidgetsArray(); + // We'll be using im.vector.integration_manager until MSC1957 or similar is accepted. + const imTypes = ["m.integration_manager", "im.vector.integration_manager"]; + return widgets.filter(w => w.content && imTypes.includes(w.content.type)); + } + /** * Remove all stickerpicker widgets (stickerpickers are user widgets by nature) * @return {Promise} Resolves on account data updated From 58ccc7eb0ceab324c668eae5405eab59a1423a0e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Aug 2019 18:01:08 -0600 Subject: [PATCH 042/413] Don't fetch widgets unless logged in --- src/integrations/IntegrationManagers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 573d251a7b..00b75fa6a7 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -67,6 +67,7 @@ export class IntegrationManagers { } _setupAccountManagers() { + if (!this._client.getUserId()) return; // not logged in const widgets = WidgetUtils.getIntegrationManagerWidgets(); widgets.forEach(w => { const data = w.content['data']; From d2c7a5a9795121ae4ea7f082c17e72ca94009c31 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Aug 2019 14:59:13 -0600 Subject: [PATCH 043/413] Also check that the client even exists --- src/integrations/IntegrationManagers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 00b75fa6a7..9c9a1fa228 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -67,7 +67,7 @@ export class IntegrationManagers { } _setupAccountManagers() { - if (!this._client.getUserId()) return; // not logged in + if (!this._client || !this._client.getUserId()) return; // not logged in const widgets = WidgetUtils.getIntegrationManagerWidgets(); widgets.forEach(w => { const data = w.content['data']; From 3e08bf6ecb5e5298ddb31fcc9afd4a06282800b7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 11 Aug 2019 03:34:12 +0100 Subject: [PATCH 044/413] Deduplicate code in ModularServerConfig by extending ServerConfig Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/auth/ModularServerConfig.js | 76 +------------------ 1 file changed, 3 insertions(+), 73 deletions(-) diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index b5af58adf1..ff8d88f738 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -15,13 +15,13 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import SdkConfig from "../../../SdkConfig"; import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import * as ServerType from '../../views/auth/ServerTypeSelector'; +import ServerConfig from "./ServerConfig"; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; @@ -33,49 +33,8 @@ const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_ * This is a variant of ServerConfig with only the HS field and different body * text that is specific to the Modular case. */ -export default class ModularServerConfig extends React.PureComponent { - static propTypes = { - onServerConfigChange: PropTypes.func, - - // The current configuration that the user is expecting to change. - serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, - - delayTimeMs: PropTypes.number, // time to wait before invoking onChanged - - // Called after the component calls onServerConfigChange - onAfterSubmit: PropTypes.func, - - // Optional text for the submit button. If falsey, no button will be shown. - submitText: PropTypes.string, - - // Optional class for the submit button. Only applies if the submit button - // is to be rendered. - submitClass: PropTypes.string, - }; - - static defaultProps = { - onServerConfigChange: function() {}, - customHsUrl: "", - delayTimeMs: 0, - }; - - constructor(props) { - super(props); - - this.state = { - busy: false, - errorText: "", - hsUrl: props.serverConfig.hsUrl, - isUrl: props.serverConfig.isUrl, - }; - } - - componentWillReceiveProps(newProps) { - if (newProps.serverConfig.hsUrl === this.state.hsUrl && - newProps.serverConfig.isUrl === this.state.isUrl) return; - - this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); - } +export default class ModularServerConfig extends ServerConfig { + static propTypes = ServerConfig.propTypes; async validateAndApplyServer(hsUrl, isUrl) { // Always try and use the defaults first @@ -120,35 +79,6 @@ export default class ModularServerConfig extends React.PureComponent { return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl); } - onHomeserverBlur = (ev) => { - this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.validateServer(); - }); - }; - - onHomeserverChange = (ev) => { - const hsUrl = ev.target.value; - this.setState({ hsUrl }); - }; - - onSubmit = async (ev) => { - ev.preventDefault(); - ev.stopPropagation(); - const result = await this.validateServer(); - if (!result) return; // Do not continue. - - if (this.props.onAfterSubmit) { - this.props.onAfterSubmit(); - } - }; - - _waitThenInvoke(existingTimeoutId, fn) { - if (existingTimeoutId) { - clearTimeout(existingTimeoutId); - } - return setTimeout(fn.bind(this), this.props.delayTimeMs); - } - render() { const Field = sdk.getComponent('elements.Field'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); From 916af736ad2445ad21168c7ae16fe10a8139693a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 11 Aug 2019 03:43:34 +0100 Subject: [PATCH 045/413] Consolidate Themes into ThemeController. Remove hardcoded themes in view Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/tabs/user/GeneralUserSettingsTab.js | 7 +++++-- src/settings/controllers/ThemeController.js | 12 +++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index a9c010b6b4..5fbc8deb35 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -1,6 +1,7 @@ /* Copyright 2019 New Vector Ltd Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,6 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; +import {SUPPORTED_THEMES} from "../../../../../settings/controllers/ThemeController"; const PlatformPeg = require("../../../../../PlatformPeg"); const MatrixClientPeg = require("../../../../../MatrixClientPeg"); const sdk = require('../../../../..'); @@ -160,8 +162,9 @@ export default class GeneralUserSettingsTab extends React.Component { {_t("Theme")} - - + {Object.entries(SUPPORTED_THEMES).map(([theme, text]) => { + return ; + })}
diff --git a/src/settings/controllers/ThemeController.js b/src/settings/controllers/ThemeController.js index 615fc4c192..fd35f79622 100644 --- a/src/settings/controllers/ThemeController.js +++ b/src/settings/controllers/ThemeController.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,16 +16,17 @@ limitations under the License. */ import SettingController from "./SettingController"; +import {_td} from "../../languageHandler"; -const SUPPORTED_THEMES = [ - "light", - "dark", -]; +export const SUPPORTED_THEMES = { + "light": _td("Light theme"), + "dark": _td("Dark theme"), +}; export default class ThemeController extends SettingController { getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) { // Override in case some no longer supported theme is stored here - if (!SUPPORTED_THEMES.includes(calculatedValue)) { + if (!SUPPORTED_THEMES[calculatedValue]) { return "light"; } From e346c1700629017321cf91936ca3e8cc0e1de7dd Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sat, 10 Aug 2019 10:27:59 +0000 Subject: [PATCH 046/413] Translated using Weblate (Albanian) Currently translated at 99.5% (1701 of 1709 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 46 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index da0d9dd91d..fd78b0cb71 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2003,5 +2003,49 @@ "I don't want to sign in": "S’dua të bëj hyrjen", "If this is a shared device, or you don't want to access your account again from it, clear all data stored locally on this device.": "Nëse kjo është një pajisje e përbashkët me të tjerë, ose nëse s’doni të hyni në llogarinë tuaj që prej saj, spastroni krejt të dhënat e depozituara lokalisht në këtë pajisje.", "Clear all data": "Spastro krejt të dhënat", - "Sign in again to regain access to your account, or a different one.": "Ribëni hyrjen që të mund të ripërdorni llogarinë tuaj, ose një tjetër." + "Sign in again to regain access to your account, or a different one.": "Ribëni hyrjen që të mund të ripërdorni llogarinë tuaj, ose një tjetër.", + "Failed to start chat": "S’u arrit të nisej fjalosje", + "Messages": "Mesazhe", + "Actions": "Veprime", + "Sends the given emote coloured as a rainbow": "E dërgon emote-n e dhënë të ngjyrosur si ylber", + "Displays list of commands with usages and descriptions": "Shfaq listë urdhrash me shembuj përdorimesh dhe përshkrime", + "Deactivate account": "Çaktivizoje llogarinë", + "Always show the window menu bar": "Shfaqe përherë shtyllën e menusë së dritares", + "Unable to revoke sharing for email address": "S’arrihet të shfuqizohet ndarja për këtë adresë email", + "Unable to share email address": "S’arrihet të ndahet adresë email", + "Check your inbox, then click Continue": "Kontrolloni email-et tuaj, mandej klikoni mbi Vazhdo", + "Revoke": "Shfuqizoje", + "Share": "Ndaje me të tjerë", + "Unable to revoke sharing for phone number": "S’arrihet të shfuqizohet ndarja për numrin e telefonit", + "Unable to share phone number": "S’arrihet të ndahet numër telefoni", + "Please enter verification code sent via text.": "Ju lutemi, jepni kod verifikimi të dërguar përmes teksti.", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Te +%(msisdn)s u dërgua një mesazh tekst. Ju lutemi, jepni kodin e verifikimit që përmban.", + "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Ju lutemi, na tregoni ç’shkoi keq ose, akoma më mirë, krijoni në GitHub një çështje që përshkruan problemin.", + "Removing…": "Po hiqet…", + "Clearing all data from this device is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Spastrimi i krejt të dhënave prej kësaj pajisjeje është përfundimtar. Mesazhet e fshehtëzuar do të humbin, veç në qofshin kopjeruajtur kyçet e tyre.", + "Share User": "Ndani Përdorues", + "Command Help": "Ndihmë Urdhri", + "Identity Server": "Shërbyes Identitetesh", + "Integrations Manager": "Përgjegjës Integrimesh", + "Find others by phone or email": "Gjeni të tjerë përmes telefoni ose email-i", + "Be found by phone or email": "Bëhuni i gjetshëm përmes telefoni ose email-i", + "Use bots, bridges, widgets and sticker packs": "Përdorni robotë, ura, widget-e dhe paketa ngjitësish", + "Terms of Service": "Kushte Shërbimi", + "To continue you need to accept the Terms of this service.": "Që të vazhdohet, lypset të pranoni Kushtet e këtij shërbimi.", + "Service": "Shërbim", + "Summary": "Përmbledhje", + "Terms": "Kushte", + "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "S’ka të formësuar Shërbyes Identitetesh, ndaj s’mund të shtoni një adresë email që të mund të ricaktoni fjalëkalimin tuaj në të ardhmen.", + "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "S’ka të formësuar Shërbyes Identitetesh: s’mund të shtohet ndonjë adresë email. S’do të jeni në gjendje të ricaktoni fjalëkalimin tuaj.", + "No identity server is configured: add one in server settings to reset your password.": "S’ka të formësuar shërbyes identitetesh: shtoni një të tillë te rregullimet e shërbyesit që të ricaktoni fjalëkalimin tuaj.", + "This account has been deactivated.": "Kjo llogari është çaktivizuar.", + "Failed to re-authenticate due to a homeserver problem": "S’u arrit të ribëhej mirëfilltësimi, për shkak të një problemi me shërbyesin Home", + "Failed to re-authenticate": "S’u arrit të ribëhej mirëfilltësimi", + "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Rifitoni hyrjen te llogaria juaj dhe rimerrni kyçe fshehtëzimi të depozituar në këtë pajisje. Pa ta, s’do të jeni në gjendje të lexoni krejt mesazhet tuaj të siguruar në çfarëdo pajisje.", + "Enter your password to sign in and regain access to your account.": "Jepni fjalëkalimin tuaj që të bëhet hyrja dhe të rifitoni hyrje në llogarinë tuaj.", + "Forgotten your password?": "Harruat fjalëkalimin tuaj?", + "Sign in and regain access to your account.": "Bëni hyrjen dhe rifitoni hyrje në llogarinë tuaj.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "S’mund të bëni hyrjen në llogarinë tuaj. Ju lutemi, për më tepër hollësi, lidhuni me përgjegjësin e shërbyesit tuaj Home.", + "Clear personal data": "Spastro të dhëna personale", + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Kujdes: Të dhënat tuaja personale (përfshi kyçe fshehtëzimi) janë ende të depozituara në këtë pajisje. Spastrojini, nëse keni përfunduar së përdoruri këtë pajisje, ose dëshironi të bëni hyrjen në një tjetër llogari." } From 66e6ed2d17a62dab138e97e7c06098a0c4226e73 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Sun, 11 Aug 2019 03:56:36 +0000 Subject: [PATCH 047/413] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1709 of 1709 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index f9c7fec1ba..944c6fcb06 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2075,5 +2075,18 @@ "Command Help": "指令說明", "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "未設定身份識別伺服器,所以您無法新增未來可以用於重設您密碼的電子郵件地址。", "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "未設定身份識別伺服器:無法新增電子郵件地址。您將無法重設您的密碼。", - "No identity server is configured: add one in server settings to reset your password.": "未設定身份識別伺服器:在伺服器設定中新增一個以重設您的密碼。" + "No identity server is configured: add one in server settings to reset your password.": "未設定身份識別伺服器:在伺服器設定中新增一個以重設您的密碼。", + "Discovery": "探索", + "Deactivate account": "停用帳號", + "Unable to revoke sharing for email address": "無法撤回電子郵件的分享", + "Unable to share email address": "無法分享電子郵件", + "Check your inbox, then click Continue": "檢查您的收件匣,然後點擊繼續", + "Revoke": "撤回", + "Share": "分享", + "Discovery options will appear once you have added an email above.": "當您在上面加入電子郵件時將會出現探索選項。", + "Unable to revoke sharing for phone number": "無法撤回電話號碼的分享", + "Unable to share phone number": "無法分享電話號碼", + "Please enter verification code sent via text.": "請輸入透過文字傳送的驗證碼。", + "Discovery options will appear once you have added a phone number above.": "當您在上面加入電話號碼時將會出現探索選項。", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "文字訊息將會被傳送到 +%(msisdn)s。請輸入其中包含的驗證碼。" } From 417d9b6af84994cfee7781c1face8e174d076fe5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 11:50:11 +0100 Subject: [PATCH 048/413] nicer way to appease linter --- src/components/views/elements/Field.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index b432bd0b8f..c414c35b0b 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -138,7 +138,7 @@ export default class Field extends React.PureComponent { render() { const { element, prefix, onValidate, children, tooltip, ...inputProps } = this.props; - !tooltip; // needs to be removed from props but we don't need it here, so otherwise unused variable + delete inputProps.tooltip; // needs to be removed from props but we don't need it here const inputElement = element || "input"; From 1067457d63e4b7f3b013233ce6ece343ee306fa0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 11:51:08 +0100 Subject: [PATCH 049/413] rerun i18n --- src/i18n/strings/en_EN.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1d7051e361..61d9fbc49e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -540,12 +540,10 @@ "Identity Server URL must be HTTPS": "Identity Server URL must be HTTPS", "Could not connect to ID Server": "Could not connect to ID Server", "Not a valid ID Server (status code %(code)s)": "Not a valid ID Server (status code %(code)s)", - "Identity Server": "Identity Server", - "Enter the URL of the Identity Server to use": "Enter the URL of the Identity Server to use", - "Looks good": "Looks good", "Checking Server": "Checking Server", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", + "Identity Server": "Identity Server", "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below": "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below", "Change": "Change", "Flair": "Flair", From bf9caa7fd891a8cde178fb8236a07f4c292d63ce Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 11:51:44 +0100 Subject: [PATCH 050/413] add the rest of the files --- res/css/views/settings/_SetIdServer.scss | 19 ++ src/components/views/settings/SetIdServer.js | 188 +++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 res/css/views/settings/_SetIdServer.scss create mode 100644 src/components/views/settings/SetIdServer.js diff --git a/res/css/views/settings/_SetIdServer.scss b/res/css/views/settings/_SetIdServer.scss new file mode 100644 index 0000000000..c6fcfc8af5 --- /dev/null +++ b/res/css/views/settings/_SetIdServer.scss @@ -0,0 +1,19 @@ +/* +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. +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. +*/ + +.mx_SetIdServer .mx_Field_input { + width: 300px; +} diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js new file mode 100644 index 0000000000..ba51de46d3 --- /dev/null +++ b/src/components/views/settings/SetIdServer.js @@ -0,0 +1,188 @@ +/* +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. +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 request from 'browser-request'; +import url from 'url'; +import React from 'react'; +import {_t} from "../../../languageHandler"; +import sdk from '../../../index'; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import SdkConfig from "../../../SdkConfig"; +import Field from "../elements/Field"; + +/** + * If a url has no path component, etc. abbreviate it to just the hostname + * + * @param {string} u The url to be abbreviated + * @returns {string} The abbreviated url + */ +function abbreviateUrl(u) { + if (!u) return ''; + + const parsedUrl = url.parse(u); + // if it's something we can't parse as a url then just return it + if (!parsedUrl) return u; + + if (parsedUrl.path == '/') { + // we ignore query / hash parts: these aren't relevant for IS server URLs + return parsedUrl.host; + } + + return u; +} + +function unabbreviateUrl(u) { + if (!u) return ''; + + let longUrl = u; + if (!u.startsWith('https://')) longUrl = 'https://' + u; + const parsed = url.parse(longUrl); + if (parsed.hostname === null) return u; + + return longUrl; +} + +/** + * Check an IS URL is valid, including liveness check + * + * @param {string} isUrl The url to check + * @returns {string} null if url passes all checks, otherwise i18ned error string + */ +async function checkIsUrl(isUrl) { + const parsedUrl = url.parse(isUrl); + + if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS"); + + // XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the + // js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it + return new Promise((resolve) => { + request( + { method: "GET", url: isUrl + '/_matrix/identity/api/v1' }, + (err, response, body) => { + if (err) { + resolve(_t("Could not connect to ID Server")); + } else if (response.status < 200 || response.status >= 300) { + resolve(_t("Not a valid ID Server (status code %(code)s)", {code: response.status})); + } else { + resolve(null); + } + }, + ); + }); +} + +export default class SetIdServer extends React.Component { + constructor() { + super(); + + let defaultIdServer = MatrixClientPeg.get().getIdentityServerUrl(); + if (!defaultIdServer) { + defaultIdServer = SdkConfig.get()['validated_server_config']['idServer'] || ''; + } + + this.state = { + currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), + idServer: defaultIdServer, + error: null, + busy: false, + }; + } + + _onIdentityServerChanged = (ev) => { + const u = ev.target.value; + + this.setState({idServer: u}); + }; + + _getTooltip = () => { + if (this.state.busy) { + const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); + return
+ + { _t("Checking Server") } +
; + } else if (this.state.error) { + return this.state.error; + } else { + return null; + } + }; + + _idServerChangeEnabled = () => { + return !!this.state.idServer && !this.state.busy; + }; + + _saveIdServer = async () => { + this.setState({busy: true}); + + const fullUrl = unabbreviateUrl(this.state.idServer); + + const errStr = await checkIsUrl(fullUrl); + if (!errStr) { + MatrixClientPeg.get().setIdentityServerUrl(fullUrl); + localStorage.setItem("mx_is_url", fullUrl); + } + this.setState({ + busy: false, + error: errStr, + currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), + idServer: '', + }); + }; + + render() { + const idServerUrl = this.state.currentClientIdServer; + let sectionTitle; + let bodyText; + if (idServerUrl) { + sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) }); + bodyText = _t( + "You are currently using to discover and be discoverable by " + + "existing contacts you know. You can change your identity server below.", + {}, + { server: sub => {abbreviateUrl(idServerUrl)} }, + ); + } else { + sectionTitle = _t("Identity Server"); + bodyText = _t( + "You are not currently using an Identity Server. " + + "To discover and be discoverable by existing contacts you know, " + + "add one below", + ); + } + + return ( +
+ + {sectionTitle} + + + {bodyText} + + + + + ); + } +} From 06905bc5bbddafad6a5c746fa901b785d81ce7bf Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 12:05:25 +0100 Subject: [PATCH 051/413] Actually appease linter --- src/components/views/elements/Field.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index c414c35b0b..084ec1bd6a 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -137,7 +137,7 @@ export default class Field extends React.PureComponent { }, VALIDATION_THROTTLE_MS); render() { - const { element, prefix, onValidate, children, tooltip, ...inputProps } = this.props; + const { element, prefix, onValidate, children, ...inputProps } = this.props; delete inputProps.tooltip; // needs to be removed from props but we don't need it here const inputElement = element || "input"; From bc66545fd0e4c9449e149dc495859ae752a878fc Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 13:11:05 +0100 Subject: [PATCH 052/413] shorten url by default --- src/components/views/settings/SetIdServer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ba51de46d3..b86b255079 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -88,9 +88,9 @@ export default class SetIdServer extends React.Component { constructor() { super(); - let defaultIdServer = MatrixClientPeg.get().getIdentityServerUrl(); + let defaultIdServer = abbreviateUrl(MatrixClientPeg.get().getIdentityServerUrl()); if (!defaultIdServer) { - defaultIdServer = SdkConfig.get()['validated_server_config']['idServer'] || ''; + defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['idServer']) || ''; } this.state = { From 10b19622db3f73c7b009418829283af5c0a6540d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 13:29:52 +0100 Subject: [PATCH 053/413] Don't clear field on failure --- src/components/views/settings/SetIdServer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index b86b255079..0140695838 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -131,15 +131,18 @@ export default class SetIdServer extends React.Component { const fullUrl = unabbreviateUrl(this.state.idServer); const errStr = await checkIsUrl(fullUrl); + + let newFormValue = this.state.idServer; if (!errStr) { MatrixClientPeg.get().setIdentityServerUrl(fullUrl); localStorage.setItem("mx_is_url", fullUrl); + newFormValue = ''; } this.setState({ busy: false, error: errStr, currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), - idServer: '', + idServer: newFormValue, }); }; From bc088e447212fc846052fae40bcc2e4721c28d21 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 13:31:31 +0100 Subject: [PATCH 054/413] add comment --- src/components/views/settings/SetIdServer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 0140695838..466ac01dd0 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -70,6 +70,8 @@ async function checkIsUrl(isUrl) { // js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it return new Promise((resolve) => { request( + // also XXX: we don't really know whether to hit /v1 or /v2 for this: we + // probably want a /versions endpoint like the C/S API. { method: "GET", url: isUrl + '/_matrix/identity/api/v1' }, (err, response, body) => { if (err) { From 36a396247a69ac93f2cc7d82aaf5c14c6daad699 Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Mon, 12 Aug 2019 11:18:20 +0000 Subject: [PATCH 055/413] Translated using Weblate (Finnish) Currently translated at 99.1% (1693 of 1709 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 5483586128..1240ea5140 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -1952,5 +1952,18 @@ "To continue you need to accept the Terms of this service.": "Jatkaaksesi sinun täytyy hyväksyä palvelun ehdot.", "Service": "Palvelu", "Summary": "Yhteenveto", - "Terms": "Ehdot" + "Terms": "Ehdot", + "Failed to start chat": "Keskustelun aloittaminen ei onnistunut", + "Messages": "Viestit", + "Actions": "Toiminnot", + "Displays list of commands with usages and descriptions": "Näyttää luettelon komennoista käyttötavoin ja kuvauksin", + "Always show the window menu bar": "Näytä aina ikkunan valikkorivi", + "Unable to revoke sharing for email address": "Sähköpostiosoitteen jakamista ei voi perua", + "Unable to share email address": "Sähköpostiosoitetta ei voi jakaa", + "Check your inbox, then click Continue": "Tarkista sähköpostisi ja klikkaa sitten Jatka", + "Share": "Jaa", + "Unable to share phone number": "Puhelinnumeroa ei voi jakaa", + "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "Identiteettipalvelinta ei ole määritetty, joten et voi lisätä sähköpostiosoitetta salasanasi palauttamiseksi vastaisuudessa.", + "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "Identiteettipalvelinta ei ole määritetty: sähköpostiosoitetta ei voi lisätä. Et pysty palauttamaan salasanaasi.", + "No identity server is configured: add one in server settings to reset your password.": "Identiteettipalvelinta ei ole määritetty: lisää se palvelinasetuksissa, jotta voi palauttaa salasanasi." } From 7bdac85a2a9f353f784320bf241378616a1a0323 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Aug 2019 14:24:12 +0100 Subject: [PATCH 056/413] Break themes out into a separate file Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../tabs/user/GeneralUserSettingsTab.js | 4 ++-- src/i18n/strings/en_EN.json | 4 ++-- src/settings/controllers/ThemeController.js | 11 +++------ src/themes.js | 24 +++++++++++++++++++ 4 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 src/themes.js diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 5fbc8deb35..4326a4f39e 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -27,7 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; -import {SUPPORTED_THEMES} from "../../../../../settings/controllers/ThemeController"; +import {THEMES} from "../../../../../themes"; const PlatformPeg = require("../../../../../PlatformPeg"); const MatrixClientPeg = require("../../../../../MatrixClientPeg"); const sdk = require('../../../../..'); @@ -162,7 +162,7 @@ export default class GeneralUserSettingsTab extends React.Component { {_t("Theme")} - {Object.entries(SUPPORTED_THEMES).map(([theme, text]) => { + {Object.entries(THEMES).map(([theme, text]) => { return ; })} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8173051c30..714e597c7a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -251,6 +251,8 @@ "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", + "Light theme": "Light theme", + "Dark theme": "Dark theme", "%(displayName)s is typing …": "%(displayName)s is typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", @@ -548,8 +550,6 @@ "Set a new account password...": "Set a new account password...", "Language and region": "Language and region", "Theme": "Theme", - "Light theme": "Light theme", - "Dark theme": "Dark theme", "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", diff --git a/src/settings/controllers/ThemeController.js b/src/settings/controllers/ThemeController.js index fd35f79622..da20521873 100644 --- a/src/settings/controllers/ThemeController.js +++ b/src/settings/controllers/ThemeController.js @@ -16,18 +16,13 @@ limitations under the License. */ import SettingController from "./SettingController"; -import {_td} from "../../languageHandler"; - -export const SUPPORTED_THEMES = { - "light": _td("Light theme"), - "dark": _td("Dark theme"), -}; +import {DEFAULT_THEME, THEMES} from "../../themes"; export default class ThemeController extends SettingController { getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) { // Override in case some no longer supported theme is stored here - if (!SUPPORTED_THEMES[calculatedValue]) { - return "light"; + if (!THEMES[calculatedValue]) { + return DEFAULT_THEME; } return null; // no override diff --git a/src/themes.js b/src/themes.js new file mode 100644 index 0000000000..1896333844 --- /dev/null +++ b/src/themes.js @@ -0,0 +1,24 @@ +/* +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + +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 {_td} from "./languageHandler"; + +export const DEFAULT_THEME = "light"; + +export const THEMES = { + "light": _td("Light theme"), + "dark": _td("Dark theme"), +}; From 384f07adb2b6d11f0fbcc518494040fe6fc73ce6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 14:36:48 +0100 Subject: [PATCH 057/413] Disconnect from IS button --- src/components/views/settings/SetIdServer.js | 48 ++++++++++++++++++++ src/i18n/strings/en_EN.json | 4 ++ 2 files changed, 52 insertions(+) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 466ac01dd0..8f7f30a2e1 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -22,6 +22,7 @@ import sdk from '../../../index'; import MatrixClientPeg from "../../../MatrixClientPeg"; import SdkConfig from "../../../SdkConfig"; import Field from "../elements/Field"; +import Modal from '../../../Modal'; /** * If a url has no path component, etc. abbreviate it to just the hostname @@ -148,7 +149,39 @@ export default class SetIdServer extends React.Component { }); }; + _onDisconnectClicked = () => { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('ID Server Disconnect Warning', '', QuestionDialog, { + title: _t("Disconnect ID Server"), + description: +
+ {_t( + "Disconnect from the ID Server ?", {}, + {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, + )}, +
, + button: _t("Disconnect"), + onFinished: (confirmed) => { + if (confirmed) { + this._disconnectIdServer(); + } + }, + }); + }; + + _disconnectIdServer = () => { + MatrixClientPeg.get().setIdentityServerUrl(null); + localStorage.removeItem("mx_is_url"); + this.setState({ + busy: false, + error: null, + currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), + idServer: '', + }); + }; + render() { + const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); const idServerUrl = this.state.currentClientIdServer; let sectionTitle; let bodyText; @@ -169,6 +202,20 @@ export default class SetIdServer extends React.Component { ); } + let discoSection; + if (idServerUrl) { + discoSection =
+ {_t( + "Disconnecting from your identity server will mean you " + + "won’t be discoverable by other users and you won’t be " + + "able to invite others by email or phone.", + )} + + {_t("Disconnect")} + +
; + } + return (
@@ -187,6 +234,7 @@ export default class SetIdServer extends React.Component { type="submit" value={_t("Change")} disabled={!this._idServerChangeEnabled()} /> + {discoSection} ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 61d9fbc49e..9c4bcef197 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -541,10 +541,14 @@ "Could not connect to ID Server": "Could not connect to ID Server", "Not a valid ID Server (status code %(code)s)": "Not a valid ID Server (status code %(code)s)", "Checking Server": "Checking Server", + "Disconnect ID Server": "Disconnect ID Server", + "Disconnect from the ID Server %(idserver)s?": "Disconnect from the ID Server %(idserver)s?", + "Disconnect": "Disconnect", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "Identity Server": "Identity Server", "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below": "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below", + "Disconnecting from your identity server will mean you won’t be discoverable by other users and you won’t be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won’t be discoverable by other users and you won’t be able to invite others by email or phone.", "Change": "Change", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", From f358b6162ddf4d227a84096e8bfc0429e0f1a5ca Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 14:38:49 +0100 Subject: [PATCH 058/413] Remove the access token for the old IS Pretty important that we don't send that to the new IS... --- src/components/views/settings/SetIdServer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 466ac01dd0..393b7aa59d 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -137,6 +137,7 @@ export default class SetIdServer extends React.Component { let newFormValue = this.state.idServer; if (!errStr) { MatrixClientPeg.get().setIdentityServerUrl(fullUrl); + localStorage.removeItem("mx_is_access_token", fullUrl); localStorage.setItem("mx_is_url", fullUrl); newFormValue = ''; } From 115e4c0370699be9b4cb2a68739e6b954721505f Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 14:40:11 +0100 Subject: [PATCH 059/413] Remove IS access token too --- src/components/views/settings/SetIdServer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 0bb92d75e4..73b8514327 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -172,6 +172,7 @@ export default class SetIdServer extends React.Component { _disconnectIdServer = () => { MatrixClientPeg.get().setIdentityServerUrl(null); + localStorage.removeItem("mx_is_access_token", fullUrl); localStorage.removeItem("mx_is_url"); this.setState({ busy: false, From 0b51a5f45260a3b9172d1df10d14b9158c884bd0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 14:45:15 +0100 Subject: [PATCH 060/413] c+p fail --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 73b8514327..b95a7f5ae5 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -172,7 +172,7 @@ export default class SetIdServer extends React.Component { _disconnectIdServer = () => { MatrixClientPeg.get().setIdentityServerUrl(null); - localStorage.removeItem("mx_is_access_token", fullUrl); + localStorage.removeItem("mx_is_access_token"); localStorage.removeItem("mx_is_url"); this.setState({ busy: false, From 4d33438acb9f2e0de9541129cef490b37f46fad1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 14:46:04 +0100 Subject: [PATCH 061/413] c+p fail --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 393b7aa59d..a87fe034a1 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -137,7 +137,7 @@ export default class SetIdServer extends React.Component { let newFormValue = this.state.idServer; if (!errStr) { MatrixClientPeg.get().setIdentityServerUrl(fullUrl); - localStorage.removeItem("mx_is_access_token", fullUrl); + localStorage.removeItem("mx_is_access_token"); localStorage.setItem("mx_is_url", fullUrl); newFormValue = ''; } From b694a56689325b7eed5a2a2f8eb8bf2510a1ee15 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Aug 2019 14:48:02 +0100 Subject: [PATCH 062/413] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9c4bcef197..577048bd18 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -542,7 +542,7 @@ "Not a valid ID Server (status code %(code)s)": "Not a valid ID Server (status code %(code)s)", "Checking Server": "Checking Server", "Disconnect ID Server": "Disconnect ID Server", - "Disconnect from the ID Server %(idserver)s?": "Disconnect from the ID Server %(idserver)s?", + "Disconnect from the ID Server ?": "Disconnect from the ID Server ?", "Disconnect": "Disconnect", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", From 387fa75da8835ad1d82bde0efd4d55395c3c0282 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Aug 2019 17:41:36 +0100 Subject: [PATCH 063/413] Bump matrix-react-test-utils for React 16 compatibility Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 2 +- yarn.lock | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 8e1a1fa668..ffd701a233 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "karma-summary-reporter": "^1.5.1", "karma-webpack": "^4.0.0-beta.0", "matrix-mock-request": "^1.2.3", - "matrix-react-test-utils": "^0.1.1", + "matrix-react-test-utils": "^0.2.2", "mocha": "^5.0.5", "react-addons-test-utils": "^15.4.0", "require-json": "0.0.1", diff --git a/yarn.lock b/yarn.lock index f6ae81d6e9..4fd19e19d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5061,13 +5061,10 @@ matrix-mock-request@^1.2.3: bluebird "^3.5.0" expect "^1.20.2" -matrix-react-test-utils@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.1.1.tgz#b548844d0ebe338ea1b9c8f16474c30d17c3bdf4" - integrity sha1-tUiETQ6+M46hucjxZHTDDRfDvfQ= - dependencies: - react "^15.6.1" - react-dom "^15.6.1" +matrix-react-test-utils@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853" + integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== md5.js@^1.3.4: version "1.3.5" @@ -6373,7 +6370,7 @@ react-beautiful-dnd@^4.0.1: redux-thunk "^2.2.0" reselect "^3.0.1" -react-dom@^15.6.0, react-dom@^15.6.1: +react-dom@^15.6.0: version "15.6.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA= @@ -6397,7 +6394,7 @@ react-dom@^16.4.2: version "2.1.5" resolved "https://codeload.github.com/matrix-org/react-gemini-scrollbar/tar.gz/f64452388011d37d8a4427ba769153c30700ab8c" dependencies: - gemini-scrollbar matrix-org/gemini-scrollbar#91e1e566 + gemini-scrollbar matrix-org/gemini-scrollbar#b302279 react-immutable-proptypes@^2.1.0: version "2.1.0" @@ -6436,7 +6433,7 @@ react-redux@^5.0.6: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" -react@^15.6.0, react@^15.6.1: +react@^15.6.0: version "15.6.2" resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI= From 7145d0e1a6cf6121fdcffa99733d07ef8506833f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Aug 2019 18:06:37 +0100 Subject: [PATCH 064/413] Hit yarn with a hammer until it works Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 4fd19e19d5..b9341b2a0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6394,7 +6394,7 @@ react-dom@^16.4.2: version "2.1.5" resolved "https://codeload.github.com/matrix-org/react-gemini-scrollbar/tar.gz/f64452388011d37d8a4427ba769153c30700ab8c" dependencies: - gemini-scrollbar matrix-org/gemini-scrollbar#b302279 + gemini-scrollbar matrix-org/gemini-scrollbar#91e1e566 react-immutable-proptypes@^2.1.0: version "2.1.0" From 438eba1c46dfa2dd69b0c0b56cf7400bdefdeafd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Aug 2019 14:14:17 -0600 Subject: [PATCH 065/413] Fix alignment of add email/phone number inputs in settings --- res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 091c98ffb8..3b330f2c30 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -23,8 +23,8 @@ limitations under the License. margin-top: 0; } -.mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses, -.mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers, +.mx_GeneralUserSettingsTab_accountSection .mx_EmailAddresses, +.mx_GeneralUserSettingsTab_accountSection .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_languageInput { margin-right: 100px; // Align with the other fields on the page } From 03d735f4ed03fd3692bf02fdb17eeead490d0239 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Aug 2019 15:35:39 -0600 Subject: [PATCH 066/413] Support changing your integration manager in the UI Part of https://github.com/vector-im/riot-web/issues/10161 --- res/css/_components.scss | 1 + .../settings/_SetIntegrationManager.scss | 34 +++++ .../views/settings/SetIntegrationManager.js | 138 ++++++++++++++++++ .../tabs/user/GeneralUserSettingsTab.js | 12 ++ src/i18n/strings/en_EN.json | 7 + .../IntegrationManagerInstance.js | 13 +- src/integrations/IntegrationManagers.js | 74 +++++++++- src/utils/WidgetUtils.js | 14 ++ 8 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 res/css/views/settings/_SetIntegrationManager.scss create mode 100644 src/components/views/settings/SetIntegrationManager.js diff --git a/res/css/_components.scss b/res/css/_components.scss index abfce47916..579369a509 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -169,6 +169,7 @@ @import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_ProfileSettings.scss"; @import "./views/settings/_SetIdServer.scss"; +@import "./views/settings/_SetIntegrationManager.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss"; diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss new file mode 100644 index 0000000000..8a1380cd1f --- /dev/null +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -0,0 +1,34 @@ +/* +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. +*/ + +.mx_SetIntegrationManager .mx_Field_input { + margin-right: 100px; // Align with the other fields on the page +} + +.mx_SetIntegrationManager { + margin-top: 10px; + margin-bottom: 10px; +} + +.mx_SetIntegrationManager > .mx_SettingsTab_heading { + margin-bottom: 10px; +} + +.mx_SetIntegrationManager > .mx_SettingsTab_heading > .mx_SettingsTab_subheading { + display: inline-block; + padding-left: 5px; + font-size: 14px; +} diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js new file mode 100644 index 0000000000..20300f548e --- /dev/null +++ b/src/components/views/settings/SetIntegrationManager.js @@ -0,0 +1,138 @@ +/* +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. +*/ + +import React from 'react'; +import {_t} from "../../../languageHandler"; +import sdk from '../../../index'; +import Field from "../elements/Field"; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; + +export default class SetIntegrationManager extends React.Component { + constructor() { + super(); + + const currentManager = IntegrationManagers.sharedInstance().getPrimaryManager(); + + this.state = { + currentManager, + url: "", // user-entered text + error: null, + busy: false, + }; + } + + _onUrlChanged = (ev) => { + const u = ev.target.value; + this.setState({url: u}); + }; + + _getTooltip = () => { + if (this.state.busy) { + const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); + return
+ + { _t("Checking server") } +
; + } else if (this.state.error) { + return this.state.error; + } else { + return null; + } + }; + + _canChange = () => { + return !!this.state.url && !this.state.busy; + }; + + _setManager = async (ev) => { + // Don't reload the page when the user hits enter in the form. + ev.preventDefault(); + ev.stopPropagation(); + + this.setState({busy: true}); + + const manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url); + if (!manager) { + this.setState({ + busy: false, + error: _t("Integration manager offline or not accessible."), + }); + return; + } + + try { + await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager); + this.setState({ + busy: false, + error: null, + currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(), + url: "", // clear input + }); + } catch (e) { + console.error(e); + this.setState({ + busy: false, + error: _t("Failed to update integration manager"), + }); + } + }; + + render() { + const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); + + const currentManager = this.state.currentManager; + let managerName; + let bodyText; + if (currentManager) { + managerName = `(${currentManager.name})`; + bodyText = _t( + "You are currently using %(serverName)s to manage your bots, widgets, " + + "and sticker packs.", + {serverName: currentManager.name}, + { b: sub => {sub} }, + ); + } else { + bodyText = _t( + "Add which integration manager you want to manage your bots, widgets, " + + "and sticker packs.", + ); + } + + return ( +
+
+ {_t("Integration Manager")} + {managerName} +
+ + {bodyText} + + + {_t("Change")} + + ); + } +} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 7e0d9f686f..0bf396c740 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -204,6 +204,17 @@ export default class GeneralUserSettingsTab extends React.Component { ); } + _renderIntegrationManagerSection() { + const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager"); + + return ( +
+ { /* has its own heading as it includes the current integration manager */ } + +
+ ); + } + render() { return (
@@ -214,6 +225,7 @@ export default class GeneralUserSettingsTab extends React.Component { {this._renderThemeSection()}
{_t("Discovery")}
{this._renderDiscoverySection()} + {this._renderIntegrationManagerSection() /* Has its own title */}
{_t("Deactivate account")}
{this._renderManagementSection()}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 154871a977..a925010f6e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -548,6 +548,13 @@ "Identity Server": "Identity Server", "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below": "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below", "Change": "Change", + "Checking server": "Checking server", + "Integration manager offline or not accessible.": "Integration manager offline or not accessible.", + "Failed to update integration manager": "Failed to update integration manager", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.", + "Integration Manager": "Integration Manager", + "Enter a new integration manager": "Enter a new integration manager", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index 4d0181f017..c21fff0fd3 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -19,12 +19,18 @@ import sdk from "../index"; import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms"; import type {Room} from "matrix-js-sdk"; import Modal from '../Modal'; +import url from 'url'; + +export const KIND_ACCOUNT = "account"; +export const KIND_CONFIG = "config"; export class IntegrationManagerInstance { apiUrl: string; uiUrl: string; + kind: string; - constructor(apiUrl: string, uiUrl: string) { + constructor(kind: string, apiUrl: string, uiUrl: string) { + this.kind = kind; this.apiUrl = apiUrl; this.uiUrl = uiUrl; @@ -32,6 +38,11 @@ export class IntegrationManagerInstance { if (!this.uiUrl) this.uiUrl = this.apiUrl; } + get name(): string { + const parsed = url.parse(this.uiUrl); + return parsed.hostname; + } + getScalarClient(): ScalarAuthClient { return new ScalarAuthClient(this.apiUrl, this.uiUrl); } diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 9c9a1fa228..9b852fe61a 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -17,7 +17,7 @@ limitations under the License. import SdkConfig from '../SdkConfig'; import sdk from "../index"; import Modal from '../Modal'; -import {IntegrationManagerInstance} from "./IntegrationManagerInstance"; +import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG} from "./IntegrationManagerInstance"; import type {MatrixClient, MatrixEvent} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; @@ -62,7 +62,7 @@ export class IntegrationManagers { const uiUrl = SdkConfig.get()['integrations_ui_url']; if (apiUrl && uiUrl) { - this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl)); + this._managers.push(new IntegrationManagerInstance(KIND_CONFIG, apiUrl, uiUrl)); } } @@ -77,7 +77,7 @@ export class IntegrationManagers { const apiUrl = data['api_url']; if (!apiUrl || !uiUrl) return; - this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl)); + this._managers.push(new IntegrationManagerInstance(KIND_ACCOUNT, apiUrl, uiUrl)); }); } @@ -107,6 +107,74 @@ export class IntegrationManagers { {configured: false}, 'mx_IntegrationsManager', ); } + + async overwriteManagerOnAccount(manager: IntegrationManagerInstance) { + // TODO: TravisR - We should be logging out of scalar clients. + await WidgetUtils.removeIntegrationManagerWidgets(); + + // TODO: TravisR - We should actually be carrying over the discovery response verbatim. + await WidgetUtils.setUserWidget( + "integration_manager_" + (new Date().getTime()), + "m.integration_manager", + manager.uiUrl, + "Integration Manager", + {"api_url": manager.apiUrl}, + ); + } + + /** + * Attempts to discover an integration manager using only its name. + * @param {string} domainName The domain name to look up. + * @returns {Promise} Resolves to an integration manager instance, + * or null if none was found. + */ + async tryDiscoverManager(domainName: string): IntegrationManagerInstance { + console.log("Looking up integration manager via .well-known"); + if (domainName.startsWith("http:") || domainName.startsWith("https:")) { + // trim off the scheme and just use the domain + const url = url.parse(domainName); + domainName = url.host; + } + + let wkConfig; + try { + const result = await fetch(`https://${domainName}/.well-known/matrix/integrations`); + wkConfig = await result.json(); + } catch (e) { + console.error(e); + console.warn("Failed to locate integration manager"); + return null; + } + + if (!wkConfig || !wkConfig["m.integrations_widget"]) { + console.warn("Missing integrations widget on .well-known response"); + return null; + } + + const widget = wkConfig["m.integrations_widget"]; + if (!widget["url"] || !widget["data"] || !widget["data"]["api_url"]) { + console.warn("Malformed .well-known response for integrations widget"); + return null; + } + + // All discovered managers are per-user managers + const manager = new IntegrationManagerInstance(KIND_ACCOUNT, widget["data"]["api_url"], widget["url"]); + console.log("Got integration manager response, checking for responsiveness"); + + // Test the manager + const client = manager.getScalarClient(); + try { + // not throwing an error is a success here + await client.connect(); + } catch (e) { + console.error(e); + console.warn("Integration manager failed liveliness check"); + return null; + } + + console.log("Integration manager is alive and functioning"); + return manager; + } } // For debugging diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 1e47554914..edac449ccf 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -351,6 +351,20 @@ export default class WidgetUtils { return widgets.filter(w => w.content && imTypes.includes(w.content.type)); } + static removeIntegrationManagerWidgets() { + const client = MatrixClientPeg.get(); + if (!client) { + throw new Error('User not logged in'); + } + const userWidgets = client.getAccountData('m.widgets').getContent() || {}; + Object.entries(userWidgets).forEach(([key, widget]) => { + if (widget.content && widget.content.type === 'm.integration_manager') { + delete userWidgets[key]; + } + }); + return client.setAccountData('m.widgets', userWidgets); + } + /** * Remove all stickerpicker widgets (stickerpickers are user widgets by nature) * @return {Promise} Resolves on account data updated From c732ae3aa99375f455f91af907c84cbcf69f1f1e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 10:46:42 +0100 Subject: [PATCH 067/413] Correct license header Co-Authored-By: J. Ryan Stinnett --- res/css/views/settings/_SetIdServer.scss | 2 +- src/components/views/settings/SetIdServer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/settings/_SetIdServer.scss b/res/css/views/settings/_SetIdServer.scss index c6fcfc8af5..cc58a61073 100644 --- a/res/css/views/settings/_SetIdServer.scss +++ b/res/css/views/settings/_SetIdServer.scss @@ -1,5 +1,5 @@ /* -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. diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index a87fe034a1..85f655a88f 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -1,5 +1,5 @@ /* -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. From 934b711936651647549675bd427460f6c9d34fda Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 10:48:56 +0100 Subject: [PATCH 068/413] write Identity Server out in full to be less confusing Co-Authored-By: J. Ryan Stinnett --- src/components/views/settings/SetIdServer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 85f655a88f..5fa3e612da 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -61,7 +61,7 @@ function unabbreviateUrl(u) { * @param {string} isUrl The url to check * @returns {string} null if url passes all checks, otherwise i18ned error string */ -async function checkIsUrl(isUrl) { +async function checkIdentityServerUrl(url) { const parsedUrl = url.parse(isUrl); if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS"); @@ -75,9 +75,9 @@ async function checkIsUrl(isUrl) { { method: "GET", url: isUrl + '/_matrix/identity/api/v1' }, (err, response, body) => { if (err) { - resolve(_t("Could not connect to ID Server")); + resolve(_t("Could not connect to Identity Server")); } else if (response.status < 200 || response.status >= 300) { - resolve(_t("Not a valid ID Server (status code %(code)s)", {code: response.status})); + resolve(_t("Not a valid Identity Server (status code %(code)s)", {code: response.status})); } else { resolve(null); } From c36c3a9b3366cb4128b86480161707827cd93424 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 10:51:26 +0100 Subject: [PATCH 069/413] other general grammar Co-Authored-By: J. Ryan Stinnett --- src/components/views/settings/SetIdServer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 5fa3e612da..b20f6930e5 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -114,7 +114,7 @@ export default class SetIdServer extends React.Component { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); return
- { _t("Checking Server") } + { _t("Checking server") }
; } else if (this.state.error) { return this.state.error; @@ -164,9 +164,9 @@ export default class SetIdServer extends React.Component { } else { sectionTitle = _t("Identity Server"); bodyText = _t( - "You are not currently using an Identity Server. " + + "You are not currently using an identity server. " + "To discover and be discoverable by existing contacts you know, " + - "add one below", + "add one below.", ); } From 860a9dbc820bb25c3a768657496ccfb9bc8ace3e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 11:01:04 +0100 Subject: [PATCH 070/413] s/tooltip/tooltipContent/ --- src/components/views/elements/Field.js | 9 ++++----- src/components/views/settings/SetIdServer.js | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 084ec1bd6a..8272b36639 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -48,7 +48,7 @@ export default class Field extends React.PureComponent { onValidate: PropTypes.func, // If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed. - tooltip: PropTypes.node, + tooltipContent: PropTypes.node, // All other props pass through to the . }; @@ -137,8 +137,7 @@ export default class Field extends React.PureComponent { }, VALIDATION_THROTTLE_MS); render() { - const { element, prefix, onValidate, children, ...inputProps } = this.props; - delete inputProps.tooltip; // needs to be removed from props but we don't need it here + const { element, prefix, onValidate, children, tooltipContent, ...inputProps } = this.props; const inputElement = element || "input"; @@ -170,11 +169,11 @@ export default class Field extends React.PureComponent { // Handle displaying feedback on validity const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; - if (this.props.tooltip || this.state.feedback) { + if (tooltipContent || this.state.feedback) { fieldTooltip = ; } diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index b20f6930e5..9fa67379d0 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -182,7 +182,7 @@ export default class SetIdServer extends React.Component { id="mx_SetIdServer_idServer" type="text" value={this.state.idServer} autoComplete="off" onChange={this._onIdentityServerChanged} - tooltip={this._getTooltip()} + tooltipContent={this._getTooltip()} /> Date: Tue, 13 Aug 2019 11:05:41 +0100 Subject: [PATCH 071/413] link to doc issue --- src/components/views/settings/SetIdServer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 9fa67379d0..a9762b6e50 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -72,6 +72,7 @@ async function checkIdentityServerUrl(url) { request( // also XXX: we don't really know whether to hit /v1 or /v2 for this: we // probably want a /versions endpoint like the C/S API. + // https://github.com/matrix-org/matrix-doc/issues/1665 { method: "GET", url: isUrl + '/_matrix/identity/api/v1' }, (err, response, body) => { if (err) { From e07c22a78d1024e678ddc7ab3900490145256d83 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 12:53:14 +0100 Subject: [PATCH 072/413] Make mixin for fields in settings that need to be the same width and make that width narrower so we can fit a tooltip in the left hand side (they were a little too wide anyway for the kind of data being entered, even on a narrow window). --- res/css/_common.scss | 4 ++++ res/css/views/settings/_ProfileSettings.scss | 4 ++++ res/css/views/settings/_SetIdServer.scss | 2 +- .../views/settings/tabs/user/_GeneralUserSettingsTab.scss | 8 ++++---- .../settings/tabs/user/_PreferencesUserSettingsTab.scss | 2 +- .../views/settings/tabs/user/_VoiceUserSettingsTab.scss | 2 +- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 517ced43fb..1b7c8ec938 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -559,3 +559,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Username_color8 { color: $username-variant8-color; } + +@define-mixin mx_Settings_fullWidthField { + margin-right: 200px; +} diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index 3e97a0ff6d..161cd7fa7a 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -30,6 +30,10 @@ limitations under the License. margin-top: 0; } +.mx_ProfileSettings_controls .mx_Field { + margin-right: 100px; +} + .mx_ProfileSettings_hostingSignup { margin-left: 20px; diff --git a/res/css/views/settings/_SetIdServer.scss b/res/css/views/settings/_SetIdServer.scss index cc58a61073..55ad6eef02 100644 --- a/res/css/views/settings/_SetIdServer.scss +++ b/res/css/views/settings/_SetIdServer.scss @@ -15,5 +15,5 @@ limitations under the License. */ .mx_SetIdServer .mx_Field_input { - width: 300px; + @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 091c98ffb8..16467165cf 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -16,15 +16,15 @@ limitations under the License. .mx_GeneralUserSettingsTab_changePassword .mx_Field, .mx_GeneralUserSettingsTab_themeSection .mx_Field { - margin-right: 100px; // Align with the other fields on the page + @mixin mx_Settings_fullWidthField; } .mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child { margin-top: 0; } -.mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses, -.mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers, +.mx_GeneralUserSettingsTab_accountSection .mx_EmailAddresses, +.mx_GeneralUserSettingsTab_accountSection .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_languageInput { - margin-right: 100px; // Align with the other fields on the page + @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index b3430f47af..d003e175d9 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -15,5 +15,5 @@ limitations under the License. */ .mx_PreferencesUserSettingsTab .mx_Field { - margin-right: 100px; // Align with the rest of the controls + @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss index 36c8cfd896..69d57bdba1 100644 --- a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_VoiceUserSettingsTab .mx_Field { - margin-right: 100px; // align with the rest of the fields + @mixin mx_Settings_fullWidthField; } .mx_VoiceUserSettingsTab_missingMediaPermissions { From 02504b995924e29b3749d75775a301763aa903c7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 12:59:34 +0100 Subject: [PATCH 073/413] make it work again --- src/components/views/settings/SetIdServer.js | 10 +++++----- src/i18n/strings/en_EN.json | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index a9762b6e50..70140d4b6a 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -58,11 +58,11 @@ function unabbreviateUrl(u) { /** * Check an IS URL is valid, including liveness check * - * @param {string} isUrl The url to check + * @param {string} u The url to check * @returns {string} null if url passes all checks, otherwise i18ned error string */ -async function checkIdentityServerUrl(url) { - const parsedUrl = url.parse(isUrl); +async function checkIdentityServerUrl(u) { + const parsedUrl = url.parse(u); if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS"); @@ -73,7 +73,7 @@ async function checkIdentityServerUrl(url) { // also XXX: we don't really know whether to hit /v1 or /v2 for this: we // probably want a /versions endpoint like the C/S API. // https://github.com/matrix-org/matrix-doc/issues/1665 - { method: "GET", url: isUrl + '/_matrix/identity/api/v1' }, + { method: "GET", url: u + '/_matrix/identity/api/v1' }, (err, response, body) => { if (err) { resolve(_t("Could not connect to Identity Server")); @@ -133,7 +133,7 @@ export default class SetIdServer extends React.Component { const fullUrl = unabbreviateUrl(this.state.idServer); - const errStr = await checkIsUrl(fullUrl); + const errStr = await checkIdentityServerUrl(fullUrl); let newFormValue = this.state.idServer; if (!errStr) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 154871a977..5576ee6122 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -540,13 +540,13 @@ "Display Name": "Display Name", "Save": "Save", "Identity Server URL must be HTTPS": "Identity Server URL must be HTTPS", - "Could not connect to ID Server": "Could not connect to ID Server", - "Not a valid ID Server (status code %(code)s)": "Not a valid ID Server (status code %(code)s)", - "Checking Server": "Checking Server", + "Could not connect to Identity Server": "Could not connect to Identity Server", + "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", + "Checking server": "Checking server", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "Identity Server": "Identity Server", - "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below": "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", "Change": "Change", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", From 050766e7bb73fba8fd7f6a2705ba82fa0e9448d2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 15:19:39 +0100 Subject: [PATCH 074/413] selector ordering --- res/css/views/settings/_ProfileSettings.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index 161cd7fa7a..afac75986f 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -26,14 +26,14 @@ limitations under the License. height: 4em; } -.mx_ProfileSettings_controls .mx_Field:first-child { - margin-top: 0; -} - .mx_ProfileSettings_controls .mx_Field { margin-right: 100px; } +.mx_ProfileSettings_controls .mx_Field:first-child { + margin-top: 0; +} + .mx_ProfileSettings_hostingSignup { margin-left: 20px; From 7a9246533d0084d954709ebbadfe1cfffc156c1b Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 15:40:03 +0100 Subject: [PATCH 075/413] Hack to ignore @define-mixin as per bug in comment --- .stylelintrc.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.stylelintrc.js b/.stylelintrc.js index 97e1ec8023..f028c76cc0 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -15,6 +15,9 @@ module.exports = { "number-leading-zero": null, "selector-list-comma-newline-after": null, "at-rule-no-unknown": null, - "scss/at-rule-no-unknown": true, + "scss/at-rule-no-unknown": [true, { + // https://github.com/vector-im/riot-web/issues/10544 + "ignoreAtRules": ["define-mixin"], + }], } } From 5bc9636e288fdcb552b96e27ddc1db2bf26dedf2 Mon Sep 17 00:00:00 2001 From: dccs Date: Tue, 13 Aug 2019 10:00:44 +0000 Subject: [PATCH 076/413] Translated using Weblate (German) Currently translated at 88.9% (1519 of 1709 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 8156248096..ab90770821 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1882,5 +1882,9 @@ "Invited by %(sender)s": "Eingeladen von %(sender)s", "Changes your avatar in all rooms": "Verändert dein Profilbild in allen Räumen", "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Dieses Gerät speichert deine Schlüssel nicht, aber du hast ein bestehendes Backup, welches du wiederherstellen kannst um fortzufahren.", - "Backup has an invalid signature from this device": "Das Backup hat eine ungültige Signatur von diesem Gerät." + "Backup has an invalid signature from this device": "Das Backup hat eine ungültige Signatur von diesem Gerät.", + "Failed to start chat": "Chat konnte nicht gestartet werden", + "Messages": "Nachrichten", + "Actions": "Aktionen", + "Displays list of commands with usages and descriptions": "Zeigt eine Liste von Befehlen mit Verwendungen und Beschreibungen an" } From 31fd36efba4eb9afca36e388ebf06a89dadac4a5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 16:20:30 +0100 Subject: [PATCH 077/413] use accessiblebutton --- src/components/views/settings/SetIdServer.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 70140d4b6a..72a47bd2ae 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -128,7 +128,9 @@ export default class SetIdServer extends React.Component { return !!this.state.idServer && !this.state.busy; }; - _saveIdServer = async () => { + _saveIdServer = async (e) => { + e.preventDefault(); + this.setState({busy: true}); const fullUrl = unabbreviateUrl(this.state.idServer); @@ -171,6 +173,7 @@ export default class SetIdServer extends React.Component { ); } + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
@@ -185,10 +188,10 @@ export default class SetIdServer extends React.Component { onChange={this._onIdentityServerChanged} tooltipContent={this._getTooltip()} /> - + onClick={this._saveIdServer} + >{_t("Change")} ); } From 2539da0dfa26ed9f11f825cfcde856f6f00ab7d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 16:37:56 +0100 Subject: [PATCH 078/413] Use fetch instead of browser-request --- src/components/views/settings/SetIdServer.js | 29 ++++++++------------ 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 72a47bd2ae..5cc63ea261 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -68,23 +68,18 @@ async function checkIdentityServerUrl(u) { // XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the // js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it - return new Promise((resolve) => { - request( - // also XXX: we don't really know whether to hit /v1 or /v2 for this: we - // probably want a /versions endpoint like the C/S API. - // https://github.com/matrix-org/matrix-doc/issues/1665 - { method: "GET", url: u + '/_matrix/identity/api/v1' }, - (err, response, body) => { - if (err) { - resolve(_t("Could not connect to Identity Server")); - } else if (response.status < 200 || response.status >= 300) { - resolve(_t("Not a valid Identity Server (status code %(code)s)", {code: response.status})); - } else { - resolve(null); - } - }, - ); - }); + try { + const response = await fetch(u + '/_matrix/identity/api/v1'); + if (response.ok) { + return null; + } else if (response.status < 200 || response.status >= 300) { + return _t("Not a valid Identity Server (status code %(code)s)", {code: response.status}); + } else { + return _t("Could not connect to Identity Server"); + } + } catch (e) { + return _t("Could not connect to Identity Server"); + } } export default class SetIdServer extends React.Component { From 596ff93049881211d94ffd312d3b1b880d3371d4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 16:38:46 +0100 Subject: [PATCH 079/413] unused import --- src/components/views/settings/SetIdServer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 5cc63ea261..9012c4893b 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import request from 'browser-request'; import url from 'url'; import React from 'react'; import {_t} from "../../../languageHandler"; From c80c8dcf244d661102ecdadb4c306c7e45d599b9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 17:55:10 +0100 Subject: [PATCH 080/413] prefill id server box with default if there isn't one --- src/components/views/settings/SetIdServer.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 9012c4893b..f1600358aa 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -85,9 +85,11 @@ export default class SetIdServer extends React.Component { constructor() { super(); - let defaultIdServer = abbreviateUrl(MatrixClientPeg.get().getIdentityServerUrl()); - if (!defaultIdServer) { - defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['idServer']) || ''; + let defaultIdServer = ''; + if (!MatrixClientPeg.get().getIdentityServerUrl() && SdkConfig.get()['validated_server_config']['isUrl']) { + // If no ID server is configured but there's one in the config, prepopulate + // the field to help the user. + defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.state = { From 6589e5dab906c8ad3591c3c51892be45c8113ec7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 13 Aug 2019 18:13:47 +0100 Subject: [PATCH 081/413] Fix showing events which were replied to and then redacted --- src/components/views/elements/ReplyThread.js | 27 ++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index ab7b1abb1c..d8955a9f28 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -176,6 +176,9 @@ export default class ReplyThread extends React.Component { componentWillMount() { this.unmounted = false; this.room = this.context.matrixClient.getRoom(this.props.parentEv.getRoomId()); + this.room.on("Room.redaction", this.onRoomRedaction); + // same event handler as Room.redaction as for both we just do forceUpdate + this.room.on("Room.redactionCancelled", this.onRoomRedaction); this.initialize(); } @@ -185,8 +188,20 @@ export default class ReplyThread extends React.Component { componentWillUnmount() { this.unmounted = true; + if (this.room) { + this.room.removeListener("Room.redaction", this.onRoomRedaction); + this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction); + } } + onRoomRedaction = (ev, room) => { + if (this.unmounted) return; + + // we could skip an update if the event isn't in our timeline, + // but that's probably an early optimisation. + this.forceUpdate(); + }; + async initialize() { const {parentEv} = this.props; // at time of making this component we checked that props.parentEv has a parentEventId @@ -298,11 +313,13 @@ export default class ReplyThread extends React.Component { return
{ dateSep } - +
; }); From a44d61fb75a9b6b4068ec415f1e09bc2d2def18e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 13 Aug 2019 18:17:42 +0100 Subject: [PATCH 082/413] add copyright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/ReplyThread.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index d8955a9f28..126b4ef017 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -1,5 +1,6 @@ /* Copyright 2017 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 7c35107a3736d9f03626a845c386f6e4ecc8df86 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 18:40:27 +0100 Subject: [PATCH 083/413] Update 3pids section visibility when id server set / unset --- src/components/views/settings/SetIdServer.js | 4 +++- .../settings/tabs/user/GeneralUserSettingsTab.js | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index f1600358aa..22b2330f33 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -20,7 +20,7 @@ import {_t} from "../../../languageHandler"; import sdk from '../../../index'; import MatrixClientPeg from "../../../MatrixClientPeg"; import SdkConfig from "../../../SdkConfig"; -import Field from "../elements/Field"; +import dis from "../../../dispatcher"; /** * If a url has no path component, etc. abbreviate it to just the hostname @@ -138,6 +138,7 @@ export default class SetIdServer extends React.Component { MatrixClientPeg.get().setIdentityServerUrl(fullUrl); localStorage.removeItem("mx_is_access_token"); localStorage.setItem("mx_is_url", fullUrl); + dis.dispatch({action: 'id_server_changed'}); newFormValue = ''; } this.setState({ @@ -170,6 +171,7 @@ export default class SetIdServer extends React.Component { } const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const Field = sdk.getComponent('elements.Field'); return (
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 7e0d9f686f..b3c7aadd7b 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -45,9 +45,22 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), + haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), }; + + this.dispatcherRef = dis.register(this._onAction); } + componentWillUnmount() { + dis.unregister(this.dispatcherRef); + } + + _onAction = (payload) => { + if (payload.action === 'id_server_changed') { + this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); + } + }; + _onLanguageChange = (newLanguage) => { if (this.state.language === newLanguage) return; @@ -124,7 +137,7 @@ export default class GeneralUserSettingsTab extends React.Component { onFinished={this._onPasswordChanged} /> ); - const threepidSection = MatrixClientPeg.get().getIdentityServerUrl() ?
+ const threepidSection = this.state.haveIdServer ?
{_t("Email addresses")} From f88349530af0a479d5a66b371955a59effcf8d68 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Aug 2019 18:41:18 +0100 Subject: [PATCH 084/413] rerun i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5576ee6122..9639ac0cd9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -540,8 +540,8 @@ "Display Name": "Display Name", "Save": "Save", "Identity Server URL must be HTTPS": "Identity Server URL must be HTTPS", - "Could not connect to Identity Server": "Could not connect to Identity Server", "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", + "Could not connect to Identity Server": "Could not connect to Identity Server", "Checking server": "Checking server", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", From b0420c106eab05f8fe8a8615d2c3430d87f3a29c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Aug 2019 10:06:05 +0100 Subject: [PATCH 085/413] Prepopulate client default on disconnect --- src/components/views/settings/SetIdServer.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 96382d9cb4..ad11b20967 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -174,11 +174,19 @@ export default class SetIdServer extends React.Component { MatrixClientPeg.get().setIdentityServerUrl(null); localStorage.removeItem("mx_is_access_token"); localStorage.removeItem("mx_is_url"); + + let newFieldVal = ''; + if (SdkConfig.get()['validated_server_config']['isUrl']) { + // Prepopulate the client's default so the user at least has some idea of + // a valid value they might enter + newFieldVal = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); + } + this.setState({ busy: false, error: null, currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), - idServer: '', + idServer: newFieldVal, }); }; From 0d560108b6c6a759758d8b50db411afca3a68405 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Aug 2019 10:07:50 +0100 Subject: [PATCH 086/413] Kill smart quotes Co-Authored-By: Travis Ralston --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ad11b20967..907add6b2d 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -218,7 +218,7 @@ export default class SetIdServer extends React.Component { discoSection =
{_t( "Disconnecting from your identity server will mean you " + - "won’t be discoverable by other users and you won’t be " + + "won't be discoverable by other users and you won't be " + "able to invite others by email or phone.", )} From 85497610e333c28831817930f808ca16cbc3c50e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Aug 2019 10:08:15 +0100 Subject: [PATCH 087/413] Spell out identity server Co-Authored-By: J. Ryan Stinnett --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 907add6b2d..334c9dfbfb 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -152,7 +152,7 @@ export default class SetIdServer extends React.Component { _onDisconnectClicked = () => { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('ID Server Disconnect Warning', '', QuestionDialog, { + Modal.createTrackedDialog('Identity Server Disconnect Warning', '', QuestionDialog, { title: _t("Disconnect ID Server"), description:
From 3c3b530e1e9569751d069516620556a09515e364 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Aug 2019 10:08:30 +0100 Subject: [PATCH 088/413] Spell out identity server Co-Authored-By: J. Ryan Stinnett --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 334c9dfbfb..05e87c3e00 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -153,7 +153,7 @@ export default class SetIdServer extends React.Component { _onDisconnectClicked = () => { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createTrackedDialog('Identity Server Disconnect Warning', '', QuestionDialog, { - title: _t("Disconnect ID Server"), + title: _t("Disconnect Identity Server"), description:
{_t( From be7956db61ea059565924e2ed8ac0ee412cd13a3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Aug 2019 10:08:43 +0100 Subject: [PATCH 089/413] Spell out identity server Co-Authored-By: J. Ryan Stinnett --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 05e87c3e00..6c6b8faab2 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -157,7 +157,7 @@ export default class SetIdServer extends React.Component { description:
{_t( - "Disconnect from the ID Server ?", {}, + "Disconnect from the identity server ?", {}, {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, )},
, From c74da125b255df6aed316a4045ab045f29030904 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Aug 2019 10:12:39 +0100 Subject: [PATCH 090/413] i18n --- src/components/views/settings/SetIdServer.js | 2 +- src/i18n/strings/en_EN.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 6c6b8faab2..398e578e8d 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -157,7 +157,7 @@ export default class SetIdServer extends React.Component { description:
{_t( - "Disconnect from the identity server ?", {}, + "Disconnect from the identity server ?", {}, {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, )},
, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3d5ee588ef..e5ecc2bf19 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -543,14 +543,14 @@ "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", "Could not connect to Identity Server": "Could not connect to Identity Server", "Checking server": "Checking server", - "Disconnect ID Server": "Disconnect ID Server", - "Disconnect from the ID Server ?": "Disconnect from the ID Server ?", + "Disconnect Identity Server": "Disconnect Identity Server", + "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect": "Disconnect", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "Identity Server": "Identity Server", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", - "Disconnecting from your identity server will mean you won’t be discoverable by other users and you won’t be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won’t be discoverable by other users and you won’t be able to invite others by email or phone.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.", "Change": "Change", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", From e668b2f8bd7eb888e4794f4d2f0388c1eab0be18 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 14 Aug 2019 12:29:48 +0100 Subject: [PATCH 091/413] delint languageHandler Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintignore.errorfiles | 1 - src/languageHandler.js | 18 +++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index e7f6ee1f84..c129f801a1 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -50,7 +50,6 @@ src/components/views/settings/Notifications.js src/GroupAddressPicker.js src/HtmlUtils.js src/ImageUtils.js -src/languageHandler.js src/linkify-matrix.js src/Markdown.js src/MatrixClientPeg.js diff --git a/src/languageHandler.js b/src/languageHandler.js index c1a426383b..474cd2b3cd 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -2,6 +2,7 @@ Copyright 2017 MTRNord and Cooperative EITA Copyright 2017 Vector Creations Ltd. Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -102,7 +103,7 @@ function safeCounterpartTranslate(text, options) { * @return a React component if any non-strings were used in substitutions, otherwise a string */ export function _t(text, variables, tags) { - // Don't do subsitutions in counterpart. We handle it ourselves so we can replace with React components + // Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components // However, still pass the variables to counterpart so that it can choose the correct plural if count is given // It is enough to pass the count variable, but in the future counterpart might make use of other information too const args = Object.assign({ interpolate: false }, variables); @@ -289,7 +290,7 @@ export function setLanguage(preferredLangs) { console.log("set language to " + langToUse); // Set 'en' as fallback language: - if (langToUse != "en") { + if (langToUse !== "en") { return getLanguage(i18nFolder + availLangs['en'].fileName); } }).then((langData) => { @@ -329,13 +330,13 @@ export function getLanguagesFromBrowser() { */ export function getNormalizedLanguageKeys(language) { const languageKeys = []; - const normalizedLanguage = this.normalizeLanguageKey(language); + const normalizedLanguage = normalizeLanguageKey(language); const languageParts = normalizedLanguage.split('-'); - if (languageParts.length == 2 && languageParts[0] == languageParts[1]) { + if (languageParts.length === 2 && languageParts[0] === languageParts[1]) { languageKeys.push(languageParts[0]); } else { languageKeys.push(normalizedLanguage); - if (languageParts.length == 2) { + if (languageParts.length === 2) { languageKeys.push(languageParts[0]); } } @@ -345,6 +346,9 @@ export function getNormalizedLanguageKeys(language) { /** * Returns a language string with underscores replaced with * hyphens, and lowercased. + * + * @param {string} language The language string to be normalized + * @returns {string} The normalized language string */ export function normalizeLanguageKey(language) { return language.toLowerCase().replace("_", "-"); @@ -373,8 +377,8 @@ export function pickBestLanguage(langs) { } { - // Failing that, a different dialect of the same lnguage - const closeLangIndex = normalisedLangs.find((l) => l.substr(0,2) === currentLang.substr(0,2)); + // Failing that, a different dialect of the same language + const closeLangIndex = normalisedLangs.find((l) => l.substr(0, 2) === currentLang.substr(0, 2)); if (closeLangIndex > -1) return langs[closeLangIndex]; } From 3342aff210259c2bd1e7bc6a7129df7f80c46d23 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 14 Aug 2019 10:05:39 +0000 Subject: [PATCH 092/413] Translated using Weblate (Albanian) Currently translated at 99.7% (1715 of 1720 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index fd78b0cb71..9dba2c9538 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2047,5 +2047,19 @@ "Sign in and regain access to your account.": "Bëni hyrjen dhe rifitoni hyrje në llogarinë tuaj.", "You cannot sign in to your account. Please contact your homeserver admin for more information.": "S’mund të bëni hyrjen në llogarinë tuaj. Ju lutemi, për më tepër hollësi, lidhuni me përgjegjësin e shërbyesit tuaj Home.", "Clear personal data": "Spastro të dhëna personale", - "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Kujdes: Të dhënat tuaja personale (përfshi kyçe fshehtëzimi) janë ende të depozituara në këtë pajisje. Spastrojini, nëse keni përfunduar së përdoruri këtë pajisje, ose dëshironi të bëni hyrjen në një tjetër llogari." + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Kujdes: Të dhënat tuaja personale (përfshi kyçe fshehtëzimi) janë ende të depozituara në këtë pajisje. Spastrojini, nëse keni përfunduar së përdoruri këtë pajisje, ose dëshironi të bëni hyrjen në një tjetër llogari.", + "Spanner": "Çelës", + "Identity Server URL must be HTTPS": "URL-ja e Shërbyesit të Identiteteve duhet të jetë HTTPS", + "Not a valid Identity Server (status code %(code)s)": "Shërbyes Identitetesh i pavlefshëm (kod gjendjeje %(code)s)", + "Could not connect to Identity Server": "S’u lidh dot me Shërbyes Identitetesh", + "Checking server": "Po kontrollohet shërbyesi", + "Disconnect Identity Server": "Shkëpute Shërbyesin e Identiteteve", + "Disconnect from the identity server ?": "Të shkëputet prej shërbyesit të identiteteve ?", + "Disconnect": "Shkëputu", + "Identity Server (%(server)s)": "Shërbyes Identitetesh (%(server)s)", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Jeni duke përdorur për të zbuluar dhe për t’u zbuluar nga kontakte ekzistues që njihni. Shërbyesin tuaj të identiteteve mund ta ndryshoni më poshtë.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistues që njihni, shtoni një të tillë më poshtë.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Shkëputja prej shërbyesit tuaj të identiteteve do të thotë se s’do të jeni i zbulueshëm nga përdorues të tjerë dhe s’do të jeni në gjendje të ftoni të tjerë përmes email-i apo telefoni.", + "Discovery options will appear once you have added an email above.": "Mundësitë e zbulimit do të shfaqen sapo të keni shtuar më sipër një email.", + "Discovery options will appear once you have added a phone number above.": "Mundësitë e zbulimit do të shfaqen sapo të keni shtuar më sipër një numër telefoni." } From 0da5c455645379a86c64ab709c4f6752c892d4d9 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 14 Aug 2019 12:23:16 +0000 Subject: [PATCH 093/413] Translated using Weblate (Chinese (Traditional)) Currently translated at 99.8% (1716 of 1720 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 944c6fcb06..fa91814296 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2088,5 +2088,12 @@ "Unable to share phone number": "無法分享電話號碼", "Please enter verification code sent via text.": "請輸入透過文字傳送的驗證碼。", "Discovery options will appear once you have added a phone number above.": "當您在上面加入電話號碼時將會出現探索選項。", - "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "文字訊息將會被傳送到 +%(msisdn)s。請輸入其中包含的驗證碼。" + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "文字訊息將會被傳送到 +%(msisdn)s。請輸入其中包含的驗證碼。", + "Identity Server URL must be HTTPS": "身份識別伺服器 URL 必須為 HTTPS", + "Not a valid Identity Server (status code %(code)s)": "不是有效的身份識別伺服器(狀態碼 %(code)s)", + "Could not connect to Identity Server": "無法連線至身份識別伺服器", + "Checking server": "正在檢查伺服器", + "Disconnect Identity Server": "斷開身份識別伺服器的連線", + "Disconnect from the identity server ?": "從身份識別伺服器 斷開連線?", + "Disconnect": "斷開連線" } From e8834930f906e37d962aa534b6a78428fed2fd0f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 14 Aug 2019 13:58:27 +0100 Subject: [PATCH 094/413] Verifying your own device should not ask you to "contact its owner" Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/dialogs/DeviceVerifyDialog.js | 16 ++++++++++++---- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index c901942fcd..88934baf5f 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -2,6 +2,7 @@ Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2019 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -241,6 +242,16 @@ export default class DeviceVerifyDialog extends React.Component { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); + let text; + if (MatrixClientPeg.get().getUserId() === this.props.userId) { + text = _t("To verify that this device can be trusted, please check that the key you see " + + "in User Settings on that device matches the key below:") + } else { + text = _t("To verify that this device can be trusted, please contact its owner using some other " + + "means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings " + + "for this device matches the key below:"); + } + const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint()); const body = (
@@ -250,10 +261,7 @@ export default class DeviceVerifyDialog extends React.Component { {_t("Use two-way text verification")}

- { _t("To verify that this device can be trusted, please contact its " + - "owner using some other means (e.g. in person or a phone call) " + - "and ask them whether the key they see in their User Settings " + - "for this device matches the key below:") } + { text }

    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 714e597c7a..09ed300abc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1176,8 +1176,9 @@ "Waiting for partner to accept...": "Waiting for partner to accept...", "Nothing appearing? Not all clients support interactive verification yet. .": "Nothing appearing? Not all clients support interactive verification yet. .", "Waiting for %(userId)s to confirm...": "Waiting for %(userId)s to confirm...", - "Use two-way text verification": "Use two-way text verification", + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:", "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:", + "Use two-way text verification": "Use two-way text verification", "Device name": "Device name", "Device key": "Device key", "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.", From 8186f1be2e03ebaa94d2d5bc1ae819cceb8f0e6c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 14 Aug 2019 14:08:10 +0100 Subject: [PATCH 095/413] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/DeviceVerifyDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 88934baf5f..710a92aa39 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -245,7 +245,7 @@ export default class DeviceVerifyDialog extends React.Component { let text; if (MatrixClientPeg.get().getUserId() === this.props.userId) { text = _t("To verify that this device can be trusted, please check that the key you see " + - "in User Settings on that device matches the key below:") + "in User Settings on that device matches the key below:"); } else { text = _t("To verify that this device can be trusted, please contact its owner using some other " + "means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings " + From 7a1a2458992a2bc728c4d6d0fbe8b23d614b2728 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Aug 2019 08:52:05 -0600 Subject: [PATCH 096/413] Fix i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e31a604fe3..22c2786922 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -552,7 +552,6 @@ "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.", "Change": "Change", - "Checking server": "Checking server", "Integration manager offline or not accessible.": "Integration manager offline or not accessible.", "Failed to update integration manager": "Failed to update integration manager", "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.", From 02a41214016d79933e521dcef88f3c8a8663b00e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Aug 2019 08:55:49 -0600 Subject: [PATCH 097/413] Mixin for field width --- res/css/views/settings/_SetIntegrationManager.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss index 8a1380cd1f..7fda042864 100644 --- a/res/css/views/settings/_SetIntegrationManager.scss +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_SetIntegrationManager .mx_Field_input { - margin-right: 100px; // Align with the other fields on the page + @mixin mx_Settings_fullWidthField; } .mx_SetIntegrationManager { From 8b1c90a01e567f9f807122ce761267d31a1e7b2f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Aug 2019 08:57:38 -0600 Subject: [PATCH 098/413] Convert to using im.vector.integration_manager for IM widget This avoids us having to throw the entirety of MSC1957 into the queue, particularly when we're only using a third of the MSC. --- src/integrations/IntegrationManagers.js | 8 +------- src/utils/WidgetUtils.js | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 9b852fe61a..49356676e6 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -113,13 +113,7 @@ export class IntegrationManagers { await WidgetUtils.removeIntegrationManagerWidgets(); // TODO: TravisR - We should actually be carrying over the discovery response verbatim. - await WidgetUtils.setUserWidget( - "integration_manager_" + (new Date().getTime()), - "m.integration_manager", - manager.uiUrl, - "Integration Manager", - {"api_url": manager.apiUrl}, - ); + await WidgetUtils.addIntegrationManagerWidget(manager.name, manager.uiUrl, manager.apiUrl); } /** diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index edac449ccf..fb79ec0ae7 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -29,6 +29,9 @@ import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; +// We'll be using im.vector.integration_manager until MSC1957 or similar is accepted. +const IM_WIDGET_TYPES = ["m.integration_manager", "im.vector.integration_manager"]; + /** * Encodes a URI according to a set of template variables. Variables will be * passed through encodeURIComponent. @@ -346,9 +349,7 @@ export default class WidgetUtils { */ static getIntegrationManagerWidgets() { const widgets = WidgetUtils.getUserWidgetsArray(); - // We'll be using im.vector.integration_manager until MSC1957 or similar is accepted. - const imTypes = ["m.integration_manager", "im.vector.integration_manager"]; - return widgets.filter(w => w.content && imTypes.includes(w.content.type)); + return widgets.filter(w => w.content && IM_WIDGET_TYPES.includes(w.content.type)); } static removeIntegrationManagerWidgets() { @@ -358,13 +359,23 @@ export default class WidgetUtils { } const userWidgets = client.getAccountData('m.widgets').getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { - if (widget.content && widget.content.type === 'm.integration_manager') { + if (widget.content && IM_WIDGET_TYPES.includes(widget.content.type)) { delete userWidgets[key]; } }); return client.setAccountData('m.widgets', userWidgets); } + static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string) { + return WidgetUtils.setUserWidget( + "integration_manager_" + (new Date().getTime()), + "im.vector.integration_manager", // TODO: Use m.integration_manager post-MSC1957 + uiUrl, + "Integration Manager: " + name, + {"api_url": apiUrl}, + ); + } + /** * Remove all stickerpicker widgets (stickerpickers are user widgets by nature) * @return {Promise} Resolves on account data updated From b77be2d3808f6784cb6dd12f5c988b9985c731fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Aug 2019 09:33:02 -0600 Subject: [PATCH 099/413] Just use MSC1957 --- src/utils/WidgetUtils.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index fb79ec0ae7..12c1578474 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -29,9 +29,6 @@ import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; -// We'll be using im.vector.integration_manager until MSC1957 or similar is accepted. -const IM_WIDGET_TYPES = ["m.integration_manager", "im.vector.integration_manager"]; - /** * Encodes a URI according to a set of template variables. Variables will be * passed through encodeURIComponent. @@ -349,7 +346,7 @@ export default class WidgetUtils { */ static getIntegrationManagerWidgets() { const widgets = WidgetUtils.getUserWidgetsArray(); - return widgets.filter(w => w.content && IM_WIDGET_TYPES.includes(w.content.type)); + return widgets.filter(w => w.content && w.content.type === "m.integration_manager"); } static removeIntegrationManagerWidgets() { @@ -359,7 +356,7 @@ export default class WidgetUtils { } const userWidgets = client.getAccountData('m.widgets').getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { - if (widget.content && IM_WIDGET_TYPES.includes(widget.content.type)) { + if (widget.content && widget.content.type === "m.integration_manager") { delete userWidgets[key]; } }); @@ -369,7 +366,7 @@ export default class WidgetUtils { static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string) { return WidgetUtils.setUserWidget( "integration_manager_" + (new Date().getTime()), - "im.vector.integration_manager", // TODO: Use m.integration_manager post-MSC1957 + "m.integration_manager", uiUrl, "Integration Manager: " + name, {"api_url": apiUrl}, From 988162e48c5ad598ecbf7a3d3f1af5b0af0d4627 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 14 Aug 2019 12:26:39 +0000 Subject: [PATCH 100/413] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1720 of 1720 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index fa91814296..fd9d497a3e 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2095,5 +2095,9 @@ "Checking server": "正在檢查伺服器", "Disconnect Identity Server": "斷開身份識別伺服器的連線", "Disconnect from the identity server ?": "從身份識別伺服器 斷開連線?", - "Disconnect": "斷開連線" + "Disconnect": "斷開連線", + "Identity Server (%(server)s)": "身份識別伺服器 (%(server)s)", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "您目前正在使用 來探索以及被您所知既有的聯絡人探索。您可以在下方變更身份識別伺服器。", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "您目前並未使用身份識別伺服器。要探索及被您所知既有的聯絡人探索,請在下方新增一個。", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "從您的身份識別伺服器斷開連線代表您不再能被其他使用者探索到,而且您也不能透過電子郵件或電話邀請其他人。" } From 0edeae643bbfbaaf8a9fd16f89e611e5357f1af6 Mon Sep 17 00:00:00 2001 From: dccs Date: Wed, 14 Aug 2019 15:27:36 +0000 Subject: [PATCH 101/413] Translated using Weblate (German) Currently translated at 88.4% (1520 of 1720 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index ab90770821..e8b771726b 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1886,5 +1886,6 @@ "Failed to start chat": "Chat konnte nicht gestartet werden", "Messages": "Nachrichten", "Actions": "Aktionen", - "Displays list of commands with usages and descriptions": "Zeigt eine Liste von Befehlen mit Verwendungen und Beschreibungen an" + "Displays list of commands with usages and descriptions": "Zeigt eine Liste von Befehlen mit Verwendungen und Beschreibungen an", + "Connect this device to Key Backup": "Schließen Sie dieses Gerät an Key Backup an" } From 7b17ea1fa52a2f4758f1bf4ff7d6b13fa3dd8fe4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 14 Aug 2019 23:15:49 +0100 Subject: [PATCH 102/413] Fix Persisted Widgets (Jitsi) randomly closing on room change Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/AppTile.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 38830d78f2..a9239303b1 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -1,6 +1,7 @@ -/** +/* Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import url from 'url'; import qs from 'querystring'; import React from 'react'; @@ -155,8 +154,9 @@ export default class AppTile extends React.Component { // Widget action listeners dis.unregister(this.dispatcherRef); + const canPersist = this.props.whitelistCapabilities.includes('m.always_on_screen'); // if it's not remaining on screen, get rid of the PersistedElement container - if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) { + if (canPersist && !ActiveWidgetStore.getWidgetPersistence(this.props.id)) { ActiveWidgetStore.destroyPersistentWidget(); const PersistedElement = sdk.getComponent("elements.PersistedElement"); PersistedElement.destroyElement(this._persistKey); @@ -575,11 +575,10 @@ export default class AppTile extends React.Component { src={this._getSafeUrl()} allowFullScreen="true" sandbox={sandboxFlags} - onLoad={this._onLoaded} - > + onLoad={this._onLoaded} />
); - // if the widget would be allowed to remian on screen, we must put it in + // if the widget would be allowed to remain on screen, we must put it in // a PersistedElement from the get-go, otherwise the iframe will be // re-mounted later when we do. if (this.props.whitelistCapabilities.includes('m.always_on_screen')) { From 6f529852cdb43b8853cd2e1115bbde0635be30ac Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 14 Aug 2019 23:27:04 +0100 Subject: [PATCH 103/413] Skip forceUpdate in ReplyThread if the redacted event is not relevant Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/ReplyThread.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 126b4ef017..08630a16a5 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -198,9 +198,10 @@ export default class ReplyThread extends React.Component { onRoomRedaction = (ev, room) => { if (this.unmounted) return; - // we could skip an update if the event isn't in our timeline, - // but that's probably an early optimisation. - this.forceUpdate(); + // If one of the events we are rendering gets redacted, force a re-render + if (this.state.events.some(event => event.getId() === ev.getId())) { + this.forceUpdate(); + } }; async initialize() { From b1a9fb79f839a7e01e6edbeb5447d6365c93f2cb Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 15 Aug 2019 04:25:38 +0000 Subject: [PATCH 104/413] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1727 of 1727 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index fd9d497a3e..51989395ad 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2099,5 +2099,12 @@ "Identity Server (%(server)s)": "身份識別伺服器 (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "您目前正在使用 來探索以及被您所知既有的聯絡人探索。您可以在下方變更身份識別伺服器。", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "您目前並未使用身份識別伺服器。要探索及被您所知既有的聯絡人探索,請在下方新增一個。", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "從您的身份識別伺服器斷開連線代表您不再能被其他使用者探索到,而且您也不能透過電子郵件或電話邀請其他人。" + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "從您的身份識別伺服器斷開連線代表您不再能被其他使用者探索到,而且您也不能透過電子郵件或電話邀請其他人。", + "Integration manager offline or not accessible.": "整合管理員離線或無法存取。", + "Failed to update integration manager": "更新整合管理員失敗", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "您目前正使用 %(serverName)s 來管理您的機器人、小工具與貼紙包。", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "新增用來管理您的機器人、小工具與貼紙包的整合管理員。", + "Integration Manager": "整合管理員", + "Enter a new integration manager": "輸入新的整合管理員", + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "要驗證此裝置是否可受信任,請檢查您在裝置上的使用者設定裡看到的金鑰是否與下方的金鑰相同:" } From b50add3715b0727139e69ef85aa2fd8d4581e5b7 Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Thu, 15 Aug 2019 09:30:00 +0000 Subject: [PATCH 105/413] Translated using Weblate (Dutch) Currently translated at 97.7% (1687 of 1727 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index c73bbbf6a4..4c3bb97118 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -152,18 +152,18 @@ "Thu": "Do", "Fri": "Vr", "Sat": "Za", - "Jan": "Jan", - "Feb": "Feb", - "Mar": "Mrt", - "Apr": "Apr", - "May": "Mei", - "Jun": "Jun", - "Jul": "Jul", - "Aug": "Aug", - "Sep": "Sep", - "Oct": "Okt", - "Nov": "Nov", - "Dec": "Dec", + "Jan": "jan", + "Feb": "feb", + "Mar": "mrt", + "Apr": "apr", + "May": "mei", + "Jun": "jun", + "Jul": "jul", + "Aug": "aug", + "Sep": "sep", + "Oct": "okt", + "Nov": "nov", + "Dec": "dec", "%(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", "%(weekDayName)s %(time)s": "%(weekDayName)s, %(time)s", From 91f544c02eeb80ddefd7204ef0c999e9a285f9e7 Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Thu, 15 Aug 2019 09:40:29 +0000 Subject: [PATCH 106/413] Translated using Weblate (Finnish) Currently translated at 98.7% (1704 of 1727 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 1240ea5140..a10b978082 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -1965,5 +1965,16 @@ "Unable to share phone number": "Puhelinnumeroa ei voi jakaa", "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "Identiteettipalvelinta ei ole määritetty, joten et voi lisätä sähköpostiosoitetta salasanasi palauttamiseksi vastaisuudessa.", "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "Identiteettipalvelinta ei ole määritetty: sähköpostiosoitetta ei voi lisätä. Et pysty palauttamaan salasanaasi.", - "No identity server is configured: add one in server settings to reset your password.": "Identiteettipalvelinta ei ole määritetty: lisää se palvelinasetuksissa, jotta voi palauttaa salasanasi." + "No identity server is configured: add one in server settings to reset your password.": "Identiteettipalvelinta ei ole määritetty: lisää se palvelinasetuksissa, jotta voi palauttaa salasanasi.", + "Identity Server URL must be HTTPS": "Identiteettipalvelimen URL-osoitteen täytyy olla HTTPS-alkuinen", + "Not a valid Identity Server (status code %(code)s)": "Ei kelvollinen identiteettipalvelin (tilakoodi %(code)s)", + "Could not connect to Identity Server": "Identiteettipalvelimeen ei saatu yhteyttä", + "Checking server": "Tarkistetaan palvelinta", + "Disconnect Identity Server": "Katkaise yhteys identiteettipalvelimeen", + "Disconnect from the identity server ?": "Katkaise yhteys identiteettipalvelimeen ?", + "Disconnect": "Katkaise yhteys", + "Identity Server (%(server)s)": "Identiteettipalvelin (%(server)s)", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Käytät palvelinta tuntemiesi henkilöiden löytämiseen ja löydetyksi tulemiseen. Voit vaihtaa identiteettipalvelintasi alla.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Et käytä tällä hetkellä identiteettipalvelinta. Lisää identiteettipalvelin alle löytääksesi tuntemiasi henkilöitä ja tullaksesi löydetyksi.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Yhteyden katkaiseminen identiteettipalvelimeesi tarkoittaa, että muut käyttäjät eivät löydä sinua etkä voi kutsua muita sähköpostin tai puhelinnumeron perusteella." } From e3013676559a8f599bae8eef8bec04dc42750fd2 Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Thu, 15 Aug 2019 09:29:10 +0000 Subject: [PATCH 107/413] Translated using Weblate (West Flemish) Currently translated at 97.7% (1687 of 1727 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/vls/ --- src/i18n/strings/vls.json | 190 +++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/src/i18n/strings/vls.json b/src/i18n/strings/vls.json index e8422febcd..9051b4b991 100644 --- a/src/i18n/strings/vls.json +++ b/src/i18n/strings/vls.json @@ -1,5 +1,5 @@ { - "This email address is already in use": "Dit e-mailadresse es al in gebruuk", + "This email address is already in use": "Dat e-mailadresse hier es al in gebruuk", "This phone number is already in use": "Dezen telefongnumero es al in gebruuk", "Failed to verify email address: make sure you clicked the link in the email": "Kostege ’t e-mailadresse nie verifieern: zorgt dervoor da je de koppelienge in den e-mail èt angeklikt", "The platform you're on": "’t Platform wuk da je gebruukt", @@ -17,10 +17,10 @@ "Your User Agent": "Je gebruukersagent", "Your device resolution": "De resolutie van je toestel", "Analytics": "Statistische gegeevns", - "The information being sent to us to help make Riot.im better includes:": "D’informatie da noar uus wor verstuurd vo Riot.im te verbetern betreft:", - "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Woar da dit blad identificeerboare informatie bevat, gelyk e gespreks-, gebruukers- of groeps-ID, goan deze gegevens verwyderd wordn voorda ze noa de server gestuurd wordn.", + "The information being sent to us to help make Riot.im better includes:": "D’informoasje da noar uus wor verstuurd vo Riot.im te verbetern betreft:", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Woar da da blad hier identificeerboare informoasje bevat, gelyk e gespreks-, gebruukers- of groeps-ID, goan deze gegevens verwyderd wordn voorda ze noa de server gestuurd wordn.", "Call Failed": "Iproep mislukt", - "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "’t Zyn ounbekende toestelln in dit gesprek: a je deuregoat zounder ze te verifieern goa ’t meuglik zyn dan ’t er etwien jen iproep afluustert.", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "’t Zyn ounbekende toestelln in da gesprek hier: a je deuregoat zounder ze te verifieern goa ’t meuglik zyn da ’t er etwien jen iproep afluustert.", "Review Devices": "Toestelln noakykn", "Call Anyway": "Algelyk belln", "Answer Anyway": "Algelyk beantwoordn", @@ -34,15 +34,15 @@ "VoIP is unsupported": "VoIP wor nie oundersteund", "You cannot place VoIP calls in this browser.": "J’en kut in deezn browser gin VoIP-iproepen pleegn.", "You cannot place a call with yourself.": "J’en ku jezelve nie belln.", - "Could not connect to the integration server": "Verbindienge me d’integroatieserver es mislukt", - "A conference call could not be started because the integrations server is not available": "Me da d’integroatieserver ounbereikboar is kostege ’t groepsaudiogesprek nie gestart wordn", + "Could not connect to the integration server": "Verbindienge me d’integroasjeserver es mislukt", + "A conference call could not be started because the integrations server is not available": "Me da d’integroasjeserver ounbereikboar is kostege ’t groepsaudiogesprek nie gestart wordn", "Call in Progress": "Loopnd gesprek", "A call is currently being placed!": "’t Wordt al een iproep gemakt!", "A call is already in progress!": "’t Es al e gesprek actief!", "Permission Required": "Toestemmienge vereist", - "You do not have permission to start a conference call in this room": "J’en èt geen toestemmienge voor in dit groepsgesprek e vergoaderiengsgesprek te begunn", + "You do not have permission to start a conference call in this room": "J’en èt geen toestemmienge voor in da groepsgesprek hier e vergoaderiengsgesprek te begunn", "Replying With Files": "Beantwoordn me bestandn", - "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Vo de moment es ’t nie meuglik van met e bestand te antwoordn. Wil je dit bestand iploadn zounder te antwoordn?", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Vo de moment es ’t nie meuglik van met e bestand te antwoordn. Wil je da bestand hier iploadn zounder te antwoordn?", "Continue": "Deuregoan", "The file '%(fileName)s' failed to upload.": "’t Bestand ‘%(fileName)s’ kostege nie gipload wordn.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "’t Bestand ‘%(fileName)s’ es groter of den iploadlimiet van den thuusserver", @@ -59,18 +59,18 @@ "Thu": "Dun", "Fri": "Vry", "Sat": "Zoa", - "Jan": "Jan", - "Feb": "Feb", - "Mar": "Mrt", - "Apr": "Apr", - "May": "Mei", - "Jun": "Jun", - "Jul": "Jul", - "Aug": "Ogu", - "Sep": "Sep", - "Oct": "Okt", - "Nov": "Nov", - "Dec": "Dec", + "Jan": "jan", + "Feb": "feb", + "Mar": "mrt", + "Apr": "apr", + "May": "mei", + "Jun": "jun", + "Jul": "jul", + "Aug": "ogu", + "Sep": "sep", + "Oct": "okt", + "Nov": "nov", + "Dec": "dec", "PM": "PM", "AM": "AM", "%(weekDayName)s %(time)s": "%(weekDayName)s, %(time)s", @@ -98,9 +98,9 @@ "Riot does not have permission to send you notifications - please check your browser settings": "Riot èt geen toestemmienge vo je meldiengn te verstuurn - controleert je browserinstelliengn", "Riot was not given permission to send notifications - please try again": "Riot èt geen toestemmienge gekreegn ghed vo joun meldiengn te verstuurn - herprobeer ’t e ki", "Unable to enable Notifications": "Kostege meldiengn nie inschoakeln", - "This email address was not found": "Dit e-mailadresse es nie gevoundn", + "This email address was not found": "Dat e-mailadresse hier es nie gevoundn", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "’t Ziet ernoar uut da jen e-mailadresse ip dezen thuusserver nie an e Matrix-ID es gekoppeld.", - "Registration Required": "Registroatie vereist", + "Registration Required": "Registroasje vereist", "You need to register to do this. Would you like to register now?": "Hiervoorn moe je je registreern. Wil je da nu doen?", "Register": "Registreern", "Default": "Standoard", @@ -112,7 +112,7 @@ "Email, name or Matrix ID": "E-mailadresse, noame, of matrix-ID", "Start Chat": "Gesprek beginn", "Invite new room members": "Nieuwe gespreksleedn uutnodign", - "Who would you like to add to this room?": "Wien wil je an dit gesprek toevoegn?", + "Who would you like to add to this room?": "Wien wil je an da gesprek hier toevoegn?", "Send Invites": "Uutnodigiengn verstuurn", "Failed to invite user": "Uutnodign van gebruuker es mislukt", "Operation failed": "Handelienge es mislukt", @@ -124,10 +124,10 @@ "Unable to create widget.": "Kostege de widget nie anmoakn.", "Missing roomId.": "roomId ountbreekt.", "Failed to send request.": "Verstuurn van verzoek es mislukt.", - "This room is not recognised.": "Dit gesprek wor nie herkend.", + "This room is not recognised.": "Da gesprek wordt hier nie herkend.", "Power level must be positive integer.": "Machtsniveau moet e positief geheel getal zyn.", - "You are not in this room.": "J’en zit nie in dit gesprek.", - "You do not have permission to do that in this room.": "J’en èt geen toestemmienge vo dat in dit gesprek te doen.", + "You are not in this room.": "J’en zit nie in ’t gesprek hier.", + "You do not have permission to do that in this room.": "J’en èt geen toestemmienge vo dat in da gesprek hier te doen.", "Missing room_id in request": "room_id ountbrikt in verzoek", "Room %(roomId)s not visible": "Gesprek %(roomId)s es nie zichtboar", "Missing user_id in request": "user_id ountbrikt in verzoek", @@ -140,16 +140,16 @@ "Room upgrade confirmation": "Bevestigienge vo ’t gesprek te actualiseern", "Upgrading a room can be destructive and isn't always necessary.": "’t Ipwoardeern van e gesprek es meuglik destructief en is nie assan noodzoakelik.", "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Gespreksipwoarderiengn wordn meestal anbevooln wanneer da der e bepoalde groepsgespreksversie als ounstabiel wor beschouwd. Ounstabiele groepsgespreksversies bevattn meuglik fouten of beveiligiengsprobleemn, of beschikkn nie over alle functies.", - "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Gespreksipwoarderiengn beïnvloedn meestal enkel de verwerkienge van ’t gesprek an serverzyde. Indien da je probleemn zoudt ounderviendn me je Riot-cliënt, gelieve dit ton te meldn ip .", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Gespreksipwoarderiengn beïnvloedn meestal enkel de verwerkienge van ’t gesprek an serverzyde. Indien da je probleemn zoudt ounderviendn me je Riot-cliënt, gelieve da ton te meldn ip .", "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. 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.": "Let ip: ’t ipwoardeern van e gesprek goa gespreksleedn nie automatisch verplatsn noa de nieuwe versie van ’t gesprek. We goan e koppelienge noa ’t nieuw gesprek in d’oude versie van ’t gesprek platsn - gespreksleedn goan ton ip deze koppeliengen moetn klikkn vo ’t nieuw gesprek toe te treedn.", - "Please confirm that you'd like to go forward with upgrading this room from to .": "Bevestigt da je dit gesprek van wilt ipwoardeern noa .", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Bevestigt da je da gesprek hier van wilt ipwoardeern noa .", "Upgrade": "Actualiseern", "Changes your display nickname": "Verandert je weergavenoame", "Changes your display nickname in the current room only": "Stelt je weergavenoame alleene moa in ’t huudig gesprek in", "Changes your avatar in this current room only": "Verandert jen avatar alleene moa in ’t huudig gesprek", "Changes colour scheme of current room": "Verandert ’t kleurenschema van ’t huudig gesprek", "Gets or sets the room topic": "Verkrygt ’t ounderwerp van ’t gesprek of stelt ’t in", - "This room has no topic.": "Dit gesprek èt geen ounderwerp.", + "This room has no topic.": "Da gesprek hier èt geen ounderwerp.", "Sets the room name": "Stelt de gespreksnoame in", "Invites user with given id to current room": "Nodigt de gebruuker me de gegeevn ID uut in ’t huudig gesprek", "Joins room with given alias": "Treedt tout ’t gesprek toe me de gegeevn bynoame", @@ -169,12 +169,12 @@ "Opens the Developer Tools dialog": "Opent de dialoogveinster me ’t ountwikkeloarsgereedschap", "Adds a custom widget by URL to the room": "Voegt met een URL een angepaste widget toe an ’t gesprek", "Please supply a https:// or http:// widget URL": "Gift een https://- of http://-widget-URL in", - "You cannot modify widgets in this room.": "J’en kut de widgets in dit gesprek nie anpassn.", - "Verifies a user, device, and pubkey tuple": "Verifieert e combinoatie van gebruuker, toestel en publieke sleuter", + "You cannot modify widgets in this room.": "J’en kut de widgets in ’t gesprek hier nie anpassn.", + "Verifies a user, device, and pubkey tuple": "Verifieert e combinoasje van gebruuker, toestel en publieke sleuter", "Unknown (user, device) pair:": "Ounbekend poar (gebruuker, toestel):", "Device already verified!": "Toestel es al geverifieerd gewist!", "WARNING: Device already verified, but keys do NOT MATCH!": "LET IP: toestel es al geverifieerd gewist, moa de sleuters KOMMN NIE OVEREEN!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "LET IP: SLEUTERVERIFICOATIE ES MISLUKT! Den oundertekende sleuter vo %(userId)s en toestel %(deviceId)s is ‘%(fprint)s’, wuk da nie overeenkomt me de verschafte sleuter ‘%(fingerprint)s’. Dit zou kunn betekenn da je communicoatie ounderschept wordt!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "LET IP: SLEUTERVERIFICOASJE ES MISLUKT! Den oundertekende sleuter vo %(userId)s en toestel %(deviceId)s is ‘%(fprint)s’, wuk da nie overeenkomt me de verschafte sleuter ‘%(fingerprint)s’. Da zoudt hier kunn betekenn da je communicoasje ounderschept wordt!", "Verified key": "Geverifieerde sleuter", "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "De versleuteriengssleuter da j’è verstrekt komt overeen me de versleuteriengssleuter da j’ountvangen èt van ’t toestel %(deviceId)s van %(userId)s. ’t Toestel es gemarkeerd als geverifieerd.", "Displays action": "Toogt actie", @@ -205,18 +205,18 @@ "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s èt ’t ounderwerp gewyzigd noa ‘%(topic)s’.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s èt de gespreksnoame verwyderd.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s èt de gespreksnoame gewyzigd noa %(roomName)s.", - "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s èt dit gesprek geactualiseerd.", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s èt da gesprek hier geactualiseerd.", "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s èt ’t gesprek toegankelik gemakt voor iedereen da de verwyzienge kent.", "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s èt ’t gesprek alleene moa ip uutnodigienge toegankelik gemakt.", "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s èt de toegangsregel veranderd noa ‘%(rule)s’", "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s èt gastn toegeloatn van ’t gesprek te betreedn.", "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s èt gastn den toegank tout ’t gesprek ountzeid.", "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s èt de toegangsregel vo gastn ip ‘%(rule)s’ ingesteld", - "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s èt badges vo %(groups)s in dit gesprek ingeschoakeld.", - "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s èt badges vo %(groups)s in dit gesprek uutgeschoakeld.", - "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s èt badges in dit gesprek vo %(newGroups)s in-, en vo %(oldGroups)s uutgeschoakeld.", + "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s èt badges vo %(groups)s in da gesprek hier ingeschoakeld.", + "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s èt badges vo %(groups)s in da gesprek hier uutgeschoakeld.", + "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s èt badges in da gesprek hier vo %(newGroups)s in-, en vo %(oldGroups)s uutgeschoakeld.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s èt e fotootje gestuurd.", - "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s èt de adressn %(addedAddresses)s an dit gesprek toegekend.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s èt de adressn %(addedAddresses)s an da gesprek hier toegekend.", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s èt %(addedAddresses)s als gespreksadresse toegevoegd.", "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s èt ’t gespreksadresse %(removedAddresses)s verwyderd.", "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s èt de gespreksadressn %(removedAddresses)s verwyderd.", @@ -250,7 +250,7 @@ "%(names)s and %(count)s others are typing …|one": "%(names)s en nog etwien zyn an ’t typn…", "%(names)s and %(lastPerson)s are typing …": "%(names)s en %(lastPerson)s zyn an ’t typn…", "No homeserver URL provided": "Geen thuusserver-URL ingegeevn", - "Unexpected error resolving homeserver configuration": "Ounverwachte foute by ’t controleern van de thuusserverconfiguroatie", + "Unexpected error resolving homeserver configuration": "Ounverwachte foute by ’t controleern van de thuusserverconfiguroasje", "This homeserver has hit its Monthly Active User limit.": "Dezen thuusserver èt z’n limiet vo moandeliks actieve gebruukers bereikt.", "This homeserver has exceeded one of its resource limits.": "Dezen thuusserver èt één van z’n systeembronlimietn overschreedn.", "Please contact your service administrator to continue using the service.": "Gelieve contact ip te neemn me je systeembeheerder vo deze dienst te bluuvn gebruukn.", @@ -271,7 +271,7 @@ "Unknown server error": "Ounbekende serverfoute", "Use a few words, avoid common phrases": "Gebruukt een antal woordn - moa geen gekende uutdrukkiengn", "No need for symbols, digits, or uppercase letters": "Hoofdletters, cyfers of specioale tekens moetn nie, moa meugn wel", - "Use a longer keyboard pattern with more turns": "Gebruukt e langer patroon me meer varioatie", + "Use a longer keyboard pattern with more turns": "Gebruukt e langer patroon me meer varioasje", "Avoid repeated words and characters": "Vermydt herhoaliengn van woordn of tekens", "Avoid sequences": "Vermydt riksjes", "Avoid recent years": "Vermydt recente joarn", @@ -340,7 +340,7 @@ "Order rooms in the room list by most important first instead of most recent": "Gesprekkn in de gesprekslyste sorteern ip belang in plekke van latste gebruuk", "Show hidden events in timeline": "Verborgn gebeurtenissn ip de tydslyn weregeevn", "Low bandwidth mode": "Lagebandbreedtemodus", - "Collecting app version information": "App-versieinformoatie wor verzoameld", + "Collecting app version information": "App-versieinformoasje wor verzoameld", "Collecting logs": "Logboekn worden verzoameld", "Uploading report": "Rapport wordt ipgeloaden", "Waiting for response from server": "Wachtn ip antwoord van de server", @@ -362,7 +362,7 @@ "Incoming call from %(name)s": "Inkommenden iproep van %(name)s", "Decline": "Weigern", "Accept": "Anveirdn", - "The other party cancelled the verification.": "De tegenparty èt de verificoatie geannuleerd.", + "The other party cancelled the verification.": "De tegenparty èt de verificoasje geannuleerd.", "Cancel": "Annuleern", "Verified!": "Geverifieerd!", "You've successfully verified this user.": "J’èt deze gebruuker geverifieerd.", @@ -370,8 +370,8 @@ "Got It": "’k Snappen ’t", "Verify this user by confirming the following emoji appear on their screen.": "Verifieert deze gebruuker deur te bevestign da zyn/heur scherm de volgende emoji toogt.", "Verify this user by confirming the following number appears on their screen.": "Verifieert deze gebruuker deur te bevestign da zyn/heur scherm ’t volgend getal toogt.", - "Unable to find a supported verification method.": "Kan geen oundersteunde verificoatiemethode viendn.", - "For maximum security, we recommend you do this in person or use another trusted means of communication.": "Vo maximoale veiligheid ku je dit best ounder vier oogn, of via een ander vertrouwd communicoatiemedium, doen.", + "Unable to find a supported verification method.": "Kan geen oundersteunde verificoasjemethode viendn.", + "For maximum security, we recommend you do this in person or use another trusted means of communication.": "Vo maximoale veiligheid ku je dit best ounder vier oogn, of via een ander vertrouwd communicoasjemedium, doen.", "Dog": "Hound", "Cat": "Katte", "Lion": "Leeuw", @@ -452,7 +452,7 @@ "Change Password": "Paswoord verandern", "Your homeserver does not support device management.": "Je thuusserver oundersteunt geen toestelbeheer.", "Unable to load device list": "Kan de lyste van toestelln nie loadn", - "Authentication": "Authenticoatie", + "Authentication": "Authenticoasje", "Delete %(count)s devices|other": "%(count)s toestelln verwydern", "Delete %(count)s devices|one": "Toestel verwydern", "Device ID": "Toestel-ID", @@ -460,7 +460,7 @@ "Last seen": "Latst gezien", "Select devices": "Selecteert toestelln", "Failed to set display name": "Instelln van weergavenoame es mislukt", - "Unable to remove contact information": "Kan contactinformoatie nie verwydern", + "Unable to remove contact information": "Kan contactinformoasje nie verwydern", "Are you sure?": "Zy je zeker?", "Yes": "Joak", "No": "Neink", @@ -528,9 +528,9 @@ "On": "An", "Noisy": "Lawoaierig", "Unable to verify phone number.": "Kostege de telefongnumero nie verifieern.", - "Incorrect verification code": "Onjuste verificoatiecode", + "Incorrect verification code": "Onjuste verificoasjecode", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "’t Es een smse verstuurd gewist noa +%(msisdn)s. Gift de verificoatiecode dervan hier in", - "Verification code": "Verificoatiecode", + "Verification code": "Verificoasjecode", "Phone Number": "Telefongnumero", "Profile picture": "Profielfoto", "Upload profile picture": "Profielfoto iploaden", @@ -615,7 +615,7 @@ "Upgrade this room to the recommended room version": "Actualiseert dit gesprek noar d’anbevooln gespreksversie", "this room": "dit gesprek", "View older messages in %(roomName)s.": "Bekykt oudere berichtn in %(roomName)s.", - "Room information": "Gespreksinformatie", + "Room information": "Gespreksinformoasje", "Internal room ID:": "Interne gespreks-ID:", "Room version": "Gespreksversie", "Room version:": "Gespreksversie:", @@ -818,7 +818,7 @@ "Re-join": "Were toetreedn", "You were banned from %(roomName)s by %(memberName)s": "Je zyt uut %(roomName)s verbann gewist deur %(memberName)s", "Something went wrong with your invite to %(roomName)s": "’t Es etwa misgegoan me jen uutnodigienge voor %(roomName)s", - "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "De foutcode %(errcode)s wier weergegeevn by ’t valideern van jen uutnodigienge. Je kut deze informoatie an e gespreksadministrator deuregeevn.", + "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "De foutcode %(errcode)s wier weergegeevn by ’t valideern van jen uutnodigienge. Je kut deze informoasje an e gespreksbeheerder deuregeevn.", "You can only join it with a working invite.": "Je kut ’t gesprek alleene moa toetreedn met e werkende uutnodigienge.", "You can still join it because this is a public room.": "Je kut ’t nog assan toetreedn, me da ’t een openboar gesprek es.", "Join the discussion": "Neemt deel an ’t gesprek", @@ -889,8 +889,8 @@ "You have disabled URL previews by default.": "J’èt URL-voorvertoniengn standoard uutgeschoakeld.", "URL previews are enabled by default for participants in this room.": "URL-voorvertoniengn zyn vo leedn van dit gesprek standoard ingeschoakeld.", "URL previews are disabled by default for participants in this room.": "URL-voorvertoniengn zyn vo leedn van dit gesprek standoard uutgeschoakeld.", - "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In versleuterde gesprekkn lyk dat hier zyn URL-voorvertoniengn standoard uutgeschoakeld, vo te voorkommn da je thuusserver (woa da de voorvertoniengn wordn gemakt) informoatie ku verzoameln over de koppeliengn da j’hiere ziet.", - "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "A ’t er etwien een URL in e bericht invoegt, kut er een URL-voorvertonienge getoogd wordn me meer informoatie over de koppelienge, gelyk den titel, omschryvienge en e fotootje van de website.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In versleuterde gesprekkn lyk dat hier zyn URL-voorvertoniengn standoard uutgeschoakeld, vo te voorkommn da je thuusserver (woa da de voorvertoniengn wordn gemakt) informoasje ku verzoameln over de koppeliengn da j’hiere ziet.", + "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "A ’t er etwien een URL in e bericht invoegt, kut er een URL-voorvertonienge getoogd wordn me meer informoasje over de koppelienge, gelyk den titel, omschryvienge en e fotootje van de website.", "Members": "Leedn", "Files": "Bestandn", "Sunday": "Zundag", @@ -923,7 +923,7 @@ "Click here to see older messages.": "Klikt hier voor oudere berichtn te bekykn.", "Copied!": "Gekopieerd!", "Failed to copy": "Kopieern mislukt", - "Add an Integration": "Voegt een integroatie toe", + "Add an Integration": "Voegt een integroasje toe", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Je goa sebiet noar en derdepartywebsite gebracht wordn zoda je den account ku legitimeern vo gebruuk me %(integrationsUrl)s. Wil je verdergoan?", "Edited at %(date)s": "Bewerkt om %(date)s", "edited": "bewerkt", @@ -1003,7 +1003,7 @@ "Rotate clockwise": "Me de klok mee droain", "Download this file": "Dit bestand downloadn", "Integrations Error": "Integroatiesfoute", - "Manage Integrations": "Integroaties beheern", + "Manage Integrations": "Integroasjes beheern", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s zyn %(count)s kis toegetreedn", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s zyn toegetreedn", @@ -1058,7 +1058,7 @@ "Edit message": "Bericht bewerkn", "Power level": "Machtsniveau", "Custom level": "Angepast niveau", - "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Kostege de gebeurtenisse woarip da der gereageerd gewist was nie loadn. Wellicht bestoa ze nie, of è je geen toeloatienge vo ze te bekykn.", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Kostege de gebeurtenisse woarip da der gereageerd gewist was nie loadn. Allichte bestoa ze nie, of è je geen toeloatienge vo ze te bekykn.", "In reply to ": "As antwoord ip ", "Room directory": "Gesprekscataloog", "And %(count)s more...|other": "En %(count)s meer…", @@ -1082,7 +1082,7 @@ "Before submitting logs, you must create a GitHub issue to describe your problem.": "Vooraleer da je logboekn indient, moe j’e meldienge openn ip GitHub woarin da je je probleem beschryft.", "GitHub issue": "GitHub-meldienge", "Notes": "Ipmerkiengn", - "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Indien da ’t er bykomende context zou kunn helpn vo ’t probleem t’analyseern, lyk wyk dan je juste an ’t doen woart, relevante gespreks-ID’s, gebruukers-ID’s, enz., gelieve deze informoatie ton hier mee te geevn.", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Indien da ’t er bykomende context zou kunn helpn vo ’t probleem t’analyseern, lyk wyk dan je juste an ’t doen woart, relevante gespreks-ID’s, gebruukers-ID’s, enz., gelieve deze informoasje ton hier mee te geevn.", "Send logs": "Logboekn verstuurn", "Unable to load commit detail: %(msg)s": "Kostege ’t commitdetail nie loadn: %(msg)s", "Unavailable": "Nie beschikboar", @@ -1123,13 +1123,13 @@ "To continue, please enter your password:": "Gif je paswoord in vo verder te goan:", "password": "paswoord", "Verify device": "Toestel verifieern", - "Use Legacy Verification (for older clients)": "Verouderde verificoatie gebruukn (voor oudere cliëntn)", + "Use Legacy Verification (for older clients)": "Verouderde verificoasje gebruukn (voor oudere cliëntn)", "Verify by comparing a short text string.": "Verifieert deur e korte tekenreekse te vergelykn.", - "Begin Verifying": "Verificoatie beginn", + "Begin Verifying": "Verificoasje beginn", "Waiting for partner to accept...": "An ’t wachtn ip de partner…", - "Nothing appearing? Not all clients support interactive verification yet. .": "Verschynt er nietent? Nog nie alle cliëntn biedn oundersteunienge voor interactieve verificoatie. .", + "Nothing appearing? Not all clients support interactive verification yet. .": "Verschynt er nietent? Nog nie alle cliëntn biedn oundersteunienge voor interactieve verificoasje. .", "Waiting for %(userId)s to confirm...": "Wachtn ip bevestigienge van %(userId)s…", - "Use two-way text verification": "Twirichtiengstekstverificoatie gebruukn", + "Use two-way text verification": "Twirichtiengstekstverificoasje gebruukn", "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "Neemt ip een andere manier (bv. ounder vier oogn of telefonisch) contact op me den eigenoar vo te controleern of da dit toestel vertrouwd ku wordn, en vroagt of da de sleuter vo dit toestel in hunder Gebruukersinstelliengn gelyk is an ounderstoande sleuter:", "Device name": "Toestelnoame", "Device key": "Toestelsleuter", @@ -1154,19 +1154,19 @@ "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verifieert deze gebruuker vo n’hem/heur als vertrouwd te markeern. Gebruukers vertrouwn gift je extra gemoedsrust by ’t gebruuk van eind-tout-eind-versleuterde berichtn.", "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Deze gebruuker verifieern goat hunder toestel als vertrouwd markeern, en ook joun toestel voor hunder als vertrouwd markeern.", "Waiting for partner to confirm...": "Wachtn ip bevestigienge van partner…", - "Incoming Verification Request": "Inkomend verificoatieverzoek", + "Incoming Verification Request": "Inkomend verificoasjeverzoek", "You added a new device '%(displayName)s', which is requesting encryption keys.": "J’èt e nieuw toestel ‘%(displayName)s’ toegevoegd, dat achter versleuteriengssleuters vroagt.", "Your unverified device '%(displayName)s' is requesting encryption keys.": "Jen oungeverifieerd toestel ‘%(displayName)s’ vroagt achter versleuteriengssleuters.", - "Start verification": "Verificoatie beginn", - "Share without verifying": "Deeln zounder verificoatie", + "Start verification": "Verificoasje beginn", + "Share without verifying": "Deeln zounder verificoasje", "Ignore request": "Verzoek negeern", - "Loading device info...": "Toestelinformoatie wor geloadn…", + "Loading device info...": "Toestelinformoasje wor geloadn…", "Encryption key request": "Verzoek vo versleuteriengssleuter", "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "J’èt al e ki Riot ip %(host)s gebruukt me lui loadn van leedn ingeschoakeld. In deze versie is lui laden uutgeschoakeld. Me da de lokoale cache nie compatibel is tusschn deze twi instelliengn, moe Riot jen account hersynchroniseern.", "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Indien dat d’andere versie van Riot nog in een ander tabblad is geopend, sluut je da best, want Riot ip dezelfsten host tegelykertyd me lui loadn ingeschoakeld en uutgeschoakeld gebruukn goa vo probleemn zorgn.", "Incompatible local cache": "Incompatibele lokoale cache", "Clear cache and resync": "Cache wissn en hersynchroniseern", - "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot verbruukt nu 3-5x minder geheugn, deur informoatie over andere gebruukers alleene moa te loadn wanneer dan ’t nodig is. Eftjes geduld, we zyn an ’t hersynchroniseern me de server!", + "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot verbruukt nu 3-5x minder geheugn, deur informoasje over andere gebruukers alleene moa te loadn wanneer dan ’t nodig is. Eftjes geduld, we zyn an ’t hersynchroniseern me de server!", "Updating Riot": "Riot wor bygewerkt", "I don't want my encrypted messages": "’k En willn ik myn versleuterde berichtn nie", "Manually export keys": "Sleuters handmatig exporteern", @@ -1194,7 +1194,7 @@ "We encountered an error trying to restore your previous session.": "’t Is e foute ipgetreedn by ’t herstelln van je vorige sessie.", "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.": "A j’al e ki gebruuk gemakt èt van e recentere versie van Riot, is je sessie meugliks ounverenigboar me deze versie. Sluut deze veinster en goa were noa de recentere versie.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "’t Legen van den ipslag van je browser goa ’t probleem misschiens verhelpn, mo goa joun ook afmeldn en gans je versleuterde gespreksgeschiedenisse ounleesboar moakn.", - "Verification Pending": "Verificoatie in afwachtienge", + "Verification Pending": "Verificoasje in afwachtienge", "Please check your email and click on the link it contains. Once this is done, click continue.": "Bekyk jen e-mails en klikt ip de koppelienge derin. Klikt van zodra da je da gedoan èt ip ‘Verdergoan’.", "Email address": "E-mailadresse", "This will allow you to reset your password and receive notifications.": "Hierdoor goa je je paswoord kunn herinstell en meldiengn ountvangn.", @@ -1227,7 +1227,7 @@ "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Sommige sessiegegeevns, inclusief sleuters vo versleuterde berichtn, ountbreekn. Meldt jen af en were an vo dit ip te lossn, en herstelt de sleuters uut den back-up.", "Your browser likely removed this data when running low on disk space.": "Je browser èt deze gegeevns meugliks verwyderd toen da de beschikboare ipslagruumte vul was.", "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Vo de moment sluut je oungeverifieerde toestelln uut; vo berichtn noa deze toestelln te verstuurn moe je ze verifieern.", - "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We roadn je an van ieder toestel te verifieern vo te kunn vastestelln of da ze tout de rechtmatigen eigenoar behoorn, moa je kut ’t bericht ook zounder verificoatie verstuurn.", + "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We roadn je an van ieder toestel te verifieern vo te kunn vastestelln of da ze tout de rechtmatigen eigenoar behoorn, moa je kut ’t bericht ook zounder verificoasje verstuurn.", "Room contains unknown devices": "’t Gesprek bevat ounbekende toestelln", "\"%(RoomName)s\" contains devices that you haven't seen before.": "‘%(RoomName)s’ bevat toestelln da je nog nie gezien ghed èt.", "Unknown devices": "Ounbekende toestelln", @@ -1314,16 +1314,16 @@ "Please review and accept all of the homeserver's policies": "Gelieve ’t beleid van de thuusserver te leezn en ’anveirdn", "Please review and accept the policies of this homeserver:": "Gelieve ’t beleid van deze thuusserver te leezn en t’anveirdn:", "An email has been sent to %(emailAddress)s": "’t Is een e-mail noa %(emailAddress)s verstuurd gewist", - "Please check your email to continue registration.": "Bekyk jen e-mails vo verder te goan me de registroatie.", + "Please check your email to continue registration.": "Bekyk jen e-mails vo verder te goan me de registroasje.", "Token incorrect": "Verkeerd bewys", "A text message has been sent to %(msisdn)s": "’t Is een smse noa %(msisdn)s verstuurd gewist", "Please enter the code it contains:": "Gift de code in da ’t er in stoat:", "Code": "Code", "Submit": "Bevestign", - "Start authentication": "Authenticoatie beginn", + "Start authentication": "Authenticoasje beginn", "Unable to validate homeserver/identity server": "Kostege de thuus-/identiteitsserver nie valideern", "Your Modular server": "Joun Modular-server", - "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "Gift de locoatie van je Modular-thuusserver in. Deze kan jen eigen domeinnoame gebruukn, of e subdomein van modular.im zyn.", + "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "Gift de locoasje van je Modular-thuusserver in. Deze kan jen eigen domeinnoame gebruukn, of e subdomein van modular.im zyn.", "Server Name": "Servernoame", "The email field must not be blank.": "’t E-mailveld meug nie leeg zyn.", "The username field must not be blank.": "’t Gebruukersnoamveld meug nie leeg zyn.", @@ -1366,7 +1366,7 @@ "Free": "Gratis", "Join millions for free on the largest public server": "Doe mee me miljoenen anderen ip de grotste publieke server", "Premium": "Premium", - "Premium hosting for organisations Learn more": "Premium hosting voor organisoaties Leest meer", + "Premium hosting for organisations Learn more": "Premium hosting voor organisoasjes Leest meer", "Other": "Overige", "Find other public servers or use a custom server": "Zoekt achter andere publieke servers, of gebruukt een angepaste server", "Sorry, your browser is not able to run Riot.": "Sorry, je browser werkt nie me Riot.", @@ -1459,7 +1459,7 @@ "Riot does not know how to join a room on this network": "Riot weet nie hoe da ’t moet deelneemn an e gesprek ip dit netwerk", "Room not found": "Gesprek nie gevoundn", "Couldn't find a matching Matrix room": "Kostege geen byhoornd Matrix-gesprek viendn", - "Fetching third party location failed": "’t Iphoaln van de locoatie van de derde party is mislukt", + "Fetching third party location failed": "’t Iphoaln van de locoasje van de derde party is mislukt", "Unable to look up room ID from server": "Kostege de gesprek-ID nie van de server iphoaln", "Search for a room": "Zoekt achter e gesprek", "Search for a room like #example": "Zoekt achter e gesprek lyk #voorbeeld", @@ -1493,7 +1493,7 @@ "Click to unmute audio": "Klikt vo ’t geluud were an te zettn", "Click to mute audio": "Klikt vo ’t geluud uut te zettn", "Clear filter": "Filter wissn", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "J’è geprobeerd e gegeven punt in de tydslyn van dit gesprek te loadn, moa j’è geen toeloatienge vo ’t desbetreffend bericht te zien.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "J’è geprobeerd van e gegeven punt in de tydslyn van dit gesprek te loadn, moa j’è geen toeloatienge vo ’t desbetreffend bericht te zien.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Geprobeerd voor e gegeven punt in de tydslyn van dit gesprek te loadn, moar kostege dit nie viendn.", "Failed to load timeline position": "Loadn van tydslynpositie is mislukt", "Guest": "Gast", @@ -1509,7 +1509,7 @@ "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Je paswoord herinstelln goat alle sleuters voor eind-tout-eind-versleuterienge ip al je toestelln herinstelln, woadeure da je versleuterde gesprekgeschiedenisse ounleesboar wordt. Stelt de sleuterback-up in of exporteer je gesprekssleuters van ip een ander toestel vooraleer da je je paswoord herinstelt.", "Your Matrix account on %(serverName)s": "Je Matrix-account ip %(serverName)s", "Your Matrix account on ": "Je Matrix-account ip ", - "A verification email will be sent to your inbox to confirm setting your new password.": "’t Is e verificoatie-e-mail noa joun gestuurd gewist vo ’t instelln van je nieuw paswoord te bevestign.", + "A verification email will be sent to your inbox to confirm setting your new password.": "’t Is e verificoasje-e-mail noa joun gestuurd gewist vo ’t instelln van je nieuw paswoord te bevestign.", "Send Reset Email": "E-mail voor herinstelln verstuurn", "Sign in instead": "Anmeldn", "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "’t Is een e-mail noar %(emailAddress)s verstuurd gewist. Klikt hieroundern van zodra da je de koppelienge derin gevolgd ghed èt.", @@ -1519,7 +1519,7 @@ "Return to login screen": "Were noa ’t anmeldiengsscherm", "Set a new password": "Stelt e nieuw paswoord in", "Invalid homeserver discovery response": "Oungeldig thuusserverountdekkiengsantwoord", - "Failed to get autodiscovery configuration from server": "Iphoaln van auto-ountdekkiengsconfiguroatie van server is mislukt", + "Failed to get autodiscovery configuration from server": "Iphoaln van auto-ountdekkiengsconfiguroasje van server is mislukt", "Invalid base_url for m.homeserver": "Oungeldige base_url vo m.homeserver", "Homeserver URL does not appear to be a valid Matrix homeserver": "De thuusserver-URL lykt geen geldige Matrix-thuusserver te zyn", "Invalid identity server discovery response": "Oungeldig identiteitsserverountdekkiengsantwoord", @@ -1533,7 +1533,7 @@ "Failed to perform homeserver discovery": "Ountdekkn van thuusserver is mislukt", "The phone number entered looks invalid": "Den ingegeevn telefongnumero ziet er oungeldig uut", "This homeserver doesn't offer any login flows which are supported by this client.": "Deze thuusserver è geen anmeldiengsmethodes da door deze cliënt wordn oundersteund.", - "Error: Problem communicating with the given homeserver.": "Foute: probleem by communicoatie me de gegeevn thuusserver.", + "Error: Problem communicating with the given homeserver.": "Foute: probleem by communicoasje me de gegeevn thuusserver.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Je ku geen verbindienge moakn me de thuusserver via HTTP wanneer dat der een HTTPS-URL in je browserbalk stoat. Gebruukt HTTPS of schoakelt ounveilige scripts in.", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Geen verbindienge met de thuusserver - controleer je verbindienge, zorgt dervoorn dan ’t SSL-certificoat van de thuusserver vertrouwd is en dat der geen browserextensies verzoekn blokkeern.", "Sign in with single sign-on": "Anmeldn met enkele anmeldienge", @@ -1541,9 +1541,9 @@ "Failed to fetch avatar URL": "Iphoaln van avatar-URL is mislukt", "Set a display name:": "Stelt e weergavenoame in:", "Upload an avatar:": "Loadt een avatar ip:", - "Registration has been disabled on this homeserver.": "Registroatie is uutgeschoakeld ip deze thuusserver.", - "Unable to query for supported registration methods.": "Kostege d’oundersteunde registroatiemethodes nie ipvroagn.", - "This server does not support authentication with a phone number.": "Deze server biedt geen oundersteunienge voor authenticoatie met e telefongnumero.", + "Registration has been disabled on this homeserver.": "Registroasje is uutgeschoakeld ip deze thuusserver.", + "Unable to query for supported registration methods.": "Kostege d’oundersteunde registroasjemethodes nie ipvroagn.", + "This server does not support authentication with a phone number.": "Deze server biedt geen oundersteunienge voor authenticoasje met e telefongnumero.", "Create your account": "Mak jen account an", "Commands": "Ipdrachtn", "Results from DuckDuckGo": "Resultoatn van DuckDuckGo", @@ -1556,7 +1556,7 @@ "Blacklisted": "Geblokkeerd", "verified": "geverifieerd", "Name": "Noame", - "Verification": "Verificoatie", + "Verification": "Verificoasje", "Ed25519 fingerprint": "Ed25519-viengerafdruk", "User ID": "Gebruukers-ID", "Curve25519 identity key": "Curve25519-identiteitssleuter", @@ -1566,7 +1566,7 @@ "unencrypted": "ounversleuterd", "Decryption error": "Ountsleuteriengsfoute", "Session ID": "Sessie-ID", - "Event information": "Gebeurtenisinformoatie", + "Event information": "Gebeurtenisinformoasje", "Sender device information": "Info over toestel van afzender", "Passphrases must match": "Paswoordn moetn overeenkommn", "Passphrase must not be empty": "Paswoord meug nie leeg zyn", @@ -1644,24 +1644,24 @@ "Cannot reach homeserver": "Kostege de thuusserver nie bereikn", "Ensure you have a stable internet connection, or get in touch with the server admin": "Zorgt da j’e stabiele internetverbiendienge èt, of neem contact op met de systeembeheerder", "Your Riot is misconfigured": "Je Riot is verkeerd geconfigureerd gewist", - "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Vroagt an je Riot-beheerder van je configuroatie noa te kykn ip verkeerde of duplicoate items.", - "Unexpected error resolving identity server configuration": "Ounverwachte foute by ’t iplossn van d’identiteitsserverconfiguroatie", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Vroagt an je Riot-beheerder van je configuroasje noa te kykn ip verkeerde of duplicoate items.", + "Unexpected error resolving identity server configuration": "Ounverwachte foute by ’t iplossn van d’identiteitsserverconfiguroasje", "Use lowercase letters, numbers, dashes and underscores only": "Gebruukt alleene moa letters, cyfers, streeptjes en underscores", "Cannot reach identity server": "Kostege den identiteitsserver nie bereikn", - "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Je ku je registreern, moa sommige functies goan pas beschikboar zyn wanneer da den identiteitsserver were online is. A je deze woarschuwienge te zien bluft krygn, controleert tan je configuroatie of nimt contact ip met e serverbeheerder.", - "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Je ku je paswoord herinstelln, moa sommige functies goan pas beschikboar zyn wanneer da den identiteitsserver were online is. A je deze woarschuwienge te zien bluft krygn, controleert tan je configuroatie of nimt contact ip met e serverbeheerder.", - "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Je ku jen anmeldn, moa sommige functies goan pas beschikboar zyn wanneer da den identiteitsserver were online is. A je deze woarschuwienge te zien bluft krygn, controleert tan je configuroatie of nimt contact ip met e serverbeheerder.", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Je ku je registreern, moa sommige functies goan pas beschikboar zyn wanneer da den identiteitsserver were online is. A je deze woarschuwienge te zien bluft krygn, controleert tan je configuroasje of nimt contact ip met e serverbeheerder.", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Je ku je paswoord herinstelln, moa sommige functies goan pas beschikboar zyn wanneer da den identiteitsserver were online is. A je deze woarschuwienge te zien bluft krygn, controleert tan je configuroasje of nimt contact ip met e serverbeheerder.", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Je ku jen anmeldn, moa sommige functies goan pas beschikboar zyn wanneer da den identiteitsserver were online is. A je deze woarschuwienge te zien bluft krygn, controleert tan je configuroasje of nimt contact ip met e serverbeheerder.", "Log in to your new account.": "Meldt jen eigen an me je nieuwen account.", "You can now close this window or log in to your new account.": "Je kut deze veinster nu sluutn, of jen eigen anmeldn me je nieuwen account.", - "Registration Successful": "Registroatie gesloagd", - "No integrations server configured": "Geen integroatieserver geconfigureerd", - "This Riot instance does not have an integrations server configured.": "Deze Riot-instantie è geen integroatieserver geconfigureerd ghed.", - "Connecting to integrations server...": "Verbiendn me den integroatieserver…", - "Cannot connect to integrations server": "Kostege geen verbiendienge moakn me den integroatieserver", - "The integrations server is offline or it cannot reach your homeserver.": "Den integroatieserver is offline of kost je thuusserver nie bereikn.", + "Registration Successful": "Registroasje gesloagd", + "No integrations server configured": "Geen integroasjeserver geconfigureerd", + "This Riot instance does not have an integrations server configured.": "Deze Riot-instantie è geen integroasjeserver geconfigureerd ghed.", + "Connecting to integrations server...": "Verbiendn me den integroasjeserver…", + "Cannot connect to integrations server": "Kostege geen verbiendienge moakn me den integroasjeserver", + "The integrations server is offline or it cannot reach your homeserver.": "Den integroasjeserver is offline of kost je thuusserver nie bereikn.", "Loading room preview": "Gespreksweergoave wor geloadn", - "Failed to connect to integrations server": "Verbiendn me den integroatieserver is mislukt", - "No integrations server is configured to manage stickers with": "’t Is geen integroatieserver geconfigureerd gewist vo stickers mee te beheern", + "Failed to connect to integrations server": "Verbiendn me den integroasjeserver is mislukt", + "No integrations server is configured to manage stickers with": "’t Is geen integroasjeserver geconfigureerd gewist vo stickers mee te beheern", "Agree": "’t Akkoord", "Disagree": "Nie ’t akkoord", "Happy": "Blye", @@ -1693,7 +1693,7 @@ "Failed to re-authenticate due to a homeserver problem": "’t Heranmeldn is mislukt omwille van e probleem me de thuusserver", "Failed to re-authenticate": "’t Heranmeldn is mislukt", "Identity Server": "Identiteitsserver", - "Integrations Manager": "Integroatiebeheerder", + "Integrations Manager": "Integroasjebeheerder", "Find others by phone or email": "Viendt andere menschn via hunder telefongnumero of e-mailadresse", "Be found by phone or email": "Wor gevoundn via je telefongnumero of e-mailadresse", "Use bots, bridges, widgets and sticker packs": "Gebruukt robottn, bruggn, widgets en stickerpakkettn", @@ -1707,7 +1707,7 @@ "Enter your password to sign in and regain access to your account.": "Voert je paswoord in vo jen an te meldn en den toegank tou jen account te herkrygn.", "Forgotten your password?": "Paswoord vergeetn?", "Sign in and regain access to your account.": "Meldt jen heran en herkrygt den toegank tou jen account.", - "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Je ku je nie anmeldn me jen account. Nimt contact ip me de beheerder van je thuusserver vo meer informoatie.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Je ku je nie anmeldn me jen account. Nimt contact ip me de beheerder van je thuusserver vo meer informoasje.", "You're signed out": "Je zyt afgemeld", "Clear personal data": "Persoonlike gegeevns wissn", "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Let ip: je persoonlike gegeevns (inclusief versleuteriengssleuters) wordn nog alsan ip dit toestel ipgesloagn. Wist ze a je gereed zyt me ’t toestel te gebruukn, of a je je wilt anmeldn me nen andern account." From 75770f7a4a039c060a99681fc235f1ebd66737b2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Aug 2019 12:04:07 +0100 Subject: [PATCH 108/413] Warn on disconnecting from IS ...if the user has bound threepids Fixes https://github.com/vector-im/riot-web/issues/10550 --- src/components/views/settings/SetIdServer.js | 66 +++++++++++++------ .../settings/discovery/EmailAddresses.js | 24 +------ .../views/settings/discovery/PhoneNumbers.js | 24 +------ src/i18n/strings/en_EN.json | 3 +- 4 files changed, 53 insertions(+), 64 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 398e578e8d..472928f43b 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -22,6 +22,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import SdkConfig from "../../../SdkConfig"; import Modal from '../../../Modal'; import dis from "../../../dispatcher"; +import { getThreepidBindStatus } from '../../../boundThreepids'; /** * If a url has no path component, etc. abbreviate it to just the hostname @@ -98,6 +99,7 @@ export default class SetIdServer extends React.Component { idServer: defaultIdServer, error: null, busy: false, + disconnectBusy: false, }; } @@ -150,24 +152,45 @@ export default class SetIdServer extends React.Component { }); }; - _onDisconnectClicked = () => { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Identity Server Disconnect Warning', '', QuestionDialog, { - title: _t("Disconnect Identity Server"), - description: -
- {_t( - "Disconnect from the identity server ?", {}, - {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, - )}, -
, - button: _t("Disconnect"), - onFinished: (confirmed) => { - if (confirmed) { - this._disconnectIdServer(); - } - }, - }); + _onDisconnectClicked = async () => { + this.setState({disconnectBusy: true}); + try { + const threepids = await getThreepidBindStatus(MatrixClientPeg.get()); + + const boundThreepids = threepids.filter(tp => tp.bound); + let message; + if (boundThreepids.length) { + message = _t( + "You are currently sharing email addresses or phone numbers on the identity " + + "server . You will need to reconnect to to stop " + + "sharing them.", {}, + { + idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + // XXX: https://github.com/vector-im/riot-web/issues/10564 + idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + } + ); + } else { + message = _t( + "Disconnect from the identity server ?", {}, + {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, + ); + } + + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Identity Server Disconnect Warning', '', QuestionDialog, { + title: _t("Disconnect Identity Server"), + description: message, + button: _t("Disconnect"), + onFinished: (confirmed) => { + if (confirmed) { + this._disconnectIdServer(); + } + }, + }); + } finally { + this.setState({disconnectBusy: false}); + } }; _disconnectIdServer = () => { @@ -215,6 +238,11 @@ export default class SetIdServer extends React.Component { let discoSection; if (idServerUrl) { + let discoButtonContent = _t("Disconnect"); + if (this.state.disconnectBusy) { + const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); + discoButtonContent = ; + } discoSection =
{_t( "Disconnecting from your identity server will mean you " + @@ -222,7 +250,7 @@ export default class SetIdServer extends React.Component { "able to invite others by email or phone.", )} - {_t("Disconnect")} + {discoButtonContent}
; } diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js index 7862eda61e..dd719bec5d 100644 --- a/src/components/views/settings/discovery/EmailAddresses.js +++ b/src/components/views/settings/discovery/EmailAddresses.js @@ -24,6 +24,7 @@ import sdk from '../../../../index'; import Modal from '../../../../Modal'; import IdentityAuthClient from '../../../../IdentityAuthClient'; import AddThreepid from '../../../../AddThreepid'; +import { getThreepidBindStatus } from '../../../../boundThreepids'; /* TODO: Improve the UX for everything in here. @@ -201,28 +202,7 @@ export default class EmailAddresses extends React.Component { const userId = client.getUserId(); const { threepids } = await client.getThreePids(); - const emails = threepids.filter((a) => a.medium === 'email'); - - if (emails.length > 0) { - // TODO: Handle terms agreement - // See https://github.com/vector-im/riot-web/issues/10522 - const authClient = new IdentityAuthClient(); - const identityAccessToken = await authClient.getAccessToken(); - - // Restructure for lookup query - const query = emails.map(({ medium, address }) => [medium, address]); - const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); - - // Record which are already bound - for (const [medium, address, mxid] of lookupResults.threepids) { - if (medium !== "email" || mxid !== userId) { - continue; - } - const email = emails.find(e => e.address === address); - if (!email) continue; - email.bound = true; - } - } + const emails = await getThreepidBindStatus(client, 'email'); this.setState({ emails }); } diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js index 3930277aea..97ca98a235 100644 --- a/src/components/views/settings/discovery/PhoneNumbers.js +++ b/src/components/views/settings/discovery/PhoneNumbers.js @@ -24,6 +24,7 @@ import sdk from '../../../../index'; import Modal from '../../../../Modal'; import IdentityAuthClient from '../../../../IdentityAuthClient'; import AddThreepid from '../../../../AddThreepid'; +import { getThreepidBindStatus } from '../../../../boundThreepids'; /* TODO: Improve the UX for everything in here. @@ -220,28 +221,7 @@ export default class PhoneNumbers extends React.Component { const userId = client.getUserId(); const { threepids } = await client.getThreePids(); - const msisdns = threepids.filter((a) => a.medium === 'msisdn'); - - if (msisdns.length > 0) { - // TODO: Handle terms agreement - // See https://github.com/vector-im/riot-web/issues/10522 - const authClient = new IdentityAuthClient(); - const identityAccessToken = await authClient.getAccessToken(); - - // Restructure for lookup query - const query = msisdns.map(({ medium, address }) => [medium, address]); - const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); - - // Record which are already bound - for (const [medium, address, mxid] of lookupResults.threepids) { - if (medium !== "msisdn" || mxid !== userId) { - continue; - } - const msisdn = msisdns.find(e => e.address === address); - if (!msisdn) continue; - msisdn.bound = true; - } - } + const msisdns = await getThreepidBindStatus(client, 'msisdn'); this.setState({ msisdns }); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e5ecc2bf19..89a12f05a5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -543,8 +543,9 @@ "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", "Could not connect to Identity Server": "Could not connect to Identity Server", "Checking server": "Checking server", - "Disconnect Identity Server": "Disconnect Identity Server", + "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", "Disconnect from the identity server ?": "Disconnect from the identity server ?", + "Disconnect Identity Server": "Disconnect Identity Server", "Disconnect": "Disconnect", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", From 54fb1b5302565fbee094b35718e7dc4dd6a5aa62 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Aug 2019 12:07:59 +0100 Subject: [PATCH 109/413] Unused variables / imports --- src/components/views/settings/discovery/EmailAddresses.js | 3 --- src/components/views/settings/discovery/PhoneNumbers.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js index dd719bec5d..4d18c1d355 100644 --- a/src/components/views/settings/discovery/EmailAddresses.js +++ b/src/components/views/settings/discovery/EmailAddresses.js @@ -22,7 +22,6 @@ import { _t } from "../../../../languageHandler"; import MatrixClientPeg from "../../../../MatrixClientPeg"; import sdk from '../../../../index'; import Modal from '../../../../Modal'; -import IdentityAuthClient from '../../../../IdentityAuthClient'; import AddThreepid from '../../../../AddThreepid'; import { getThreepidBindStatus } from '../../../../boundThreepids'; @@ -199,9 +198,7 @@ export default class EmailAddresses extends React.Component { async componentWillMount() { const client = MatrixClientPeg.get(); - const userId = client.getUserId(); - const { threepids } = await client.getThreePids(); const emails = await getThreepidBindStatus(client, 'email'); this.setState({ emails }); diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js index 97ca98a235..fdebac5d22 100644 --- a/src/components/views/settings/discovery/PhoneNumbers.js +++ b/src/components/views/settings/discovery/PhoneNumbers.js @@ -22,7 +22,6 @@ import { _t } from "../../../../languageHandler"; import MatrixClientPeg from "../../../../MatrixClientPeg"; import sdk from '../../../../index'; import Modal from '../../../../Modal'; -import IdentityAuthClient from '../../../../IdentityAuthClient'; import AddThreepid from '../../../../AddThreepid'; import { getThreepidBindStatus } from '../../../../boundThreepids'; @@ -218,9 +217,7 @@ export default class PhoneNumbers extends React.Component { async componentWillMount() { const client = MatrixClientPeg.get(); - const userId = client.getUserId(); - const { threepids } = await client.getThreePids(); const msisdns = await getThreepidBindStatus(client, 'msisdn'); this.setState({ msisdns }); From e6c5775d0efc21f887f562f041e57faefb92ce8a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Aug 2019 12:16:59 +0100 Subject: [PATCH 110/413] Commit the new file --- src/boundThreepids.js | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/boundThreepids.js diff --git a/src/boundThreepids.js b/src/boundThreepids.js new file mode 100644 index 0000000000..799728f801 --- /dev/null +++ b/src/boundThreepids.js @@ -0,0 +1,52 @@ +/* +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. +*/ + +import IdentityAuthClient from './IdentityAuthClient'; + +export async function getThreepidBindStatus(client, filterMedium) { + const userId = client.getUserId(); + + let { threepids } = await client.getThreePids(); + if (filterMedium) { + threepids = threepids.filter((a) => a.medium === filterMedium); + } + + if (threepids.length > 0) { + // TODO: Handle terms agreement + // See https://github.com/vector-im/riot-web/issues/10522 + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken(); + + // Restructure for lookup query + const query = threepids.map(({ medium, address }) => [medium, address]); + const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); + + // Record which are already bound + for (const [medium, address, mxid] of lookupResults.threepids) { + if (mxid !== userId) { + continue; + } + if (filterMedium && medium !== filterMedium) { + continue; + } + const threepid = threepids.find(e => e.medium === medium && e.address === address); + if (!threepid) continue; + threepid.bound = true; + } + } + + return threepids; +} From 6f07f9157c733b9c203c1b35b78f162384e3bd6e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Aug 2019 13:10:05 +0100 Subject: [PATCH 111/413] lint --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 472928f43b..3a67d124ae 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -168,7 +168,7 @@ export default class SetIdServer extends React.Component { idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, // XXX: https://github.com/vector-im/riot-web/issues/10564 idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - } + }, ); } else { message = _t( From 40f693d0fce77edf61d83bd4b77f452a041ecf53 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Aug 2019 14:55:59 +0100 Subject: [PATCH 112/413] Fix set integration manager tooltip The name of the prop changed in the original PR and this one didn't get updated. --- src/components/views/settings/SetIntegrationManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index 20300f548e..8d26fdf40e 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -124,7 +124,7 @@ export default class SetIntegrationManager extends React.Component { id="mx_SetIntegrationManager_newUrl" type="text" value={this.state.url} autoComplete="off" onChange={this._onUrlChanged} - tooltip={this._getTooltip()} + tooltipContent={this._getTooltip()} /> Date: Tue, 13 Aug 2019 15:50:36 +0100 Subject: [PATCH 113/413] Prompt for ICE server fallback permission This adds a prompt at the start of each session when the homeserver does not have any ICE servers configured. The fallback ICE server is only used if the user allows it. The dialog also recommends notifying the homeserver admin to rectify the issue. Fixes https://github.com/vector-im/riot-web/issues/10173 --- src/components/structures/MatrixChat.js | 31 +++++++++++++++++++++++++ src/i18n/strings/en_EN.json | 4 ++++ 2 files changed, 35 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b8903076c7..ff30bb1605 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1370,6 +1370,37 @@ export default React.createClass({ call: call, }, true); }); + cli.on('Call.noTURNServers', () => { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const code = sub => {sub}; + Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { + title: _t('Homeserver not configured to support calls'), + description:
+

{ _t( + "Your homeserver %(homeserverDomain)s is " + + "currently not configured to assist with calls by offering a " + + "TURN server, which means it is likely that voice and video " + + "calls will fail. Please notify your homeserver administrator " + + "so that they can address this.", + { homeserverDomain: cli.getDomain() }, { code }, + ) }

+

{ _t( + "Riot can use a fallback server turn.matrix.org " + + "for the current session if you urgently need to make a call. " + + "Your IP address would be shared with this fallback server " + + "only if you agree and later place or receive a call.", + null, { code }, + )}

+
, + button: _t('Allow Fallback'), + cancelButton: _t('Dismiss'), + onFinished: (confirmed) => { + if (confirmed) { + cli.setFallbackICEServerAllowed(true); + } + }, + }, null, true); + }); cli.on('Session.logged_out', function(errObj) { if (Lifecycle.isLoggingOut()) return; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0ca1ece48f..ecfdc5642a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1507,6 +1507,10 @@ "Failed to leave room": "Failed to leave room", "Can't leave Server Notices room": "Can't leave Server Notices room", "This room is used for important messages from the Homeserver, so you cannot leave it.": "This room is used for important messages from the Homeserver, so you cannot leave it.", + "Homeserver not configured to support calls": "Homeserver not configured to support calls", + "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.": "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.", + "Riot can use a fallback server turn.matrix.org for the current session if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call.": "Riot can use a fallback server turn.matrix.org for the current session if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call.", + "Allow Fallback": "Allow Fallback", "Signed Out": "Signed Out", "For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.", "Terms and Conditions": "Terms and Conditions", From 1c6312d99950aa0a34a355e378fc43929dd66fd4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 14 Aug 2019 14:02:25 +0100 Subject: [PATCH 114/413] Store ICE fallback permission in device setting This stores the ICE server fallback permission in a device setting so it is remembered across sessions. Part of https://github.com/matrix-org/matrix-react-sdk/pull/3309 --- src/MatrixClientPeg.js | 1 + src/components/structures/MatrixChat.js | 14 +++++++------- .../settings/tabs/user/VoiceUserSettingsTab.js | 15 ++++++++++++++- src/i18n/strings/en_EN.json | 3 ++- src/settings/Settings.js | 6 ++++++ 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index d2760bc82c..813f0ed87e 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -216,6 +216,7 @@ class MatrixClientPeg { deviceId: creds.deviceId, timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), + fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), verificationMethods: [verificationMethods.SAS], unstableClientRelationAggregation: true, }; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ff30bb1605..f935303c0a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1386,18 +1386,18 @@ export default React.createClass({ ) }

{ _t( "Riot can use a fallback server turn.matrix.org " + - "for the current session if you urgently need to make a call. " + - "Your IP address would be shared with this fallback server " + - "only if you agree and later place or receive a call.", + "if you urgently need to make a call. Your IP address would be " + + "shared with this fallback server only if you agree and later " + + "place or receive a call. You can change this permission later " + + "in the Voice & Video section of Settings.", null, { code }, )}

, button: _t('Allow Fallback'), cancelButton: _t('Dismiss'), - onFinished: (confirmed) => { - if (confirmed) { - cli.setFallbackICEServerAllowed(true); - } + onFinished: (allow) => { + SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); + cli.setFallbackICEServerAllowed(allow); }, }, null, true); }); diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index eb85fe4e44..18ea5a82be 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -115,6 +115,10 @@ export default class VoiceUserSettingsTab extends React.Component { MatrixClientPeg.get().setForceTURN(!p2p); }; + _changeFallbackICEServerAllowed = (allow) => { + MatrixClientPeg.get().setFallbackICEServerAllowed(allow); + }; + _renderDeviceOptions(devices, category) { return devices.map((d) => { return (); @@ -201,7 +205,16 @@ export default class VoiceUserSettingsTab extends React.Component { {microphoneDropdown} {webcamDropdown} - + +
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ecfdc5642a..c74b19a223 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -355,6 +355,7 @@ "Show recently visited rooms above the room list": "Show recently visited rooms above the room list", "Show hidden events in timeline": "Show hidden events in timeline", "Low bandwidth mode": "Low bandwidth mode", + "Allow fallback call assist server turn.matrix.org": "Allow fallback call assist server turn.matrix.org", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", @@ -1509,7 +1510,7 @@ "This room is used for important messages from the Homeserver, so you cannot leave it.": "This room is used for important messages from the Homeserver, so you cannot leave it.", "Homeserver not configured to support calls": "Homeserver not configured to support calls", "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.": "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.", - "Riot can use a fallback server turn.matrix.org for the current session if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call.": "Riot can use a fallback server turn.matrix.org for the current session if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call.", + "Riot can use a fallback server turn.matrix.org if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call. You can change this permission later in the Voice & Video section of Settings.": "Riot can use a fallback server turn.matrix.org if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call. You can change this permission later in the Voice & Video section of Settings.", "Allow Fallback": "Allow Fallback", "Signed Out": "Signed Out", "For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 55085963d1..77e1c2cb25 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -372,4 +372,10 @@ export const SETTINGS = { default: false, controller: new LowBandwidthController(), }, + "fallbackICEServerAllowed": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + displayName: _td("Allow fallback call assist server turn.matrix.org"), + // This is a tri-state value, where `null` means "prompt the user". + default: null, + }, }; From d31f556c1fd982e053bb535d278acd1dbe2add24 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 14 Aug 2019 15:06:06 +0100 Subject: [PATCH 115/413] Move ICE fallback prompt to time of placing / answering calls This moves the ICE fallback prompt out of session startup and instead it will now appear contextually when your either place a call with no ICE server from the homeserver or a call fails (in either direction). Fixes https://github.com/vector-im/riot-web/issues/10546 --- src/CallHandler.js | 55 +++++++++++++++++++++++-- src/components/structures/MatrixChat.js | 31 -------------- src/i18n/strings/en_EN.json | 10 ++--- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 40a8d426f8..a7feb74beb 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017, 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. @@ -64,6 +65,7 @@ import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; import {IntegrationManagers} from "./integrations/IntegrationManagers"; +import SettingsStore, { SettingLevel } from './settings/SettingsStore'; global.mxCalls = { //room_id: MatrixCall @@ -117,8 +119,7 @@ function _reAttemptCall(call) { function _setCallListeners(call) { call.on("error", function(err) { - console.error("Call error: %s", err); - console.error(err.stack); + console.error("Call error:", err); if (err.code === 'unknown_devices') { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -146,8 +147,15 @@ function _setCallListeners(call) { }, }); } else { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + if ( + MatrixClientPeg.get().getTurnServers().length === 0 && + SettingsStore.getValue("fallbackICEServerAllowed") === null + ) { + _showICEFallbackPrompt(_t("Call Failed")); + return; + } + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { title: _t('Call Failed'), description: err.message, @@ -217,6 +225,39 @@ function _setCallState(call, roomId, status) { }); } +function _showICEFallbackPrompt(title) { + const cli = MatrixClientPeg.get(); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const code = sub => {sub}; + Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { + title, + description:
+

{ _t( + "Your homeserver %(homeserverDomain)s is " + + "currently not configured to assist with calls by offering a " + + "TURN server, which means it is likely that voice and video " + + "calls will fail. Please notify your homeserver administrator " + + "so that they can address this.", + { homeserverDomain: cli.getDomain() }, { code }, + ) }

+

{ _t( + "Riot can use a fallback server turn.matrix.org " + + "if you urgently need to make a call. Your IP address would be " + + "shared with this fallback server only if you agree and later " + + "place or receive a call. You can change this permission later " + + "in the Voice & Video section of Settings.", + null, { code }, + )}

+
, + button: _t('Allow Fallback'), + cancelButton: _t('Dismiss'), + onFinished: (allow) => { + SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); + cli.setFallbackICEServerAllowed(allow); + }, + }, null, true); +} + function _onAction(payload) { function placeCall(newCall) { _setCallListeners(newCall); @@ -270,6 +311,14 @@ function _onAction(payload) { return; } + if ( + MatrixClientPeg.get().getTurnServers().length === 0 && + SettingsStore.getValue("fallbackICEServerAllowed") === null + ) { + _showICEFallbackPrompt(_t("Homeserver not configured to support calls")); + return; + } + const room = MatrixClientPeg.get().getRoom(payload.room_id); if (!room) { console.error("Room %s does not exist.", payload.room_id); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index f935303c0a..b8903076c7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1370,37 +1370,6 @@ export default React.createClass({ call: call, }, true); }); - cli.on('Call.noTURNServers', () => { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const code = sub => {sub}; - Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { - title: _t('Homeserver not configured to support calls'), - description:
-

{ _t( - "Your homeserver %(homeserverDomain)s is " + - "currently not configured to assist with calls by offering a " + - "TURN server, which means it is likely that voice and video " + - "calls will fail. Please notify your homeserver administrator " + - "so that they can address this.", - { homeserverDomain: cli.getDomain() }, { code }, - ) }

-

{ _t( - "Riot can use a fallback server turn.matrix.org " + - "if you urgently need to make a call. Your IP address would be " + - "shared with this fallback server only if you agree and later " + - "place or receive a call. You can change this permission later " + - "in the Voice & Video section of Settings.", - null, { code }, - )}

-
, - button: _t('Allow Fallback'), - cancelButton: _t('Dismiss'), - onFinished: (allow) => { - SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); - cli.setFallbackICEServerAllowed(allow); - }, - }, null, true); - }); cli.on('Session.logged_out', function(errObj) { if (Lifecycle.isLoggingOut()) return; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c74b19a223..ddf7a6baa7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -28,11 +28,16 @@ "Answer": "Answer", "Call Timeout": "Call Timeout", "The remote side failed to pick up": "The remote side failed to pick up", + "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.": "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.", + "Riot can use a fallback server turn.matrix.org if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call. You can change this permission later in the Voice & Video section of Settings.": "Riot can use a fallback server turn.matrix.org if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call. You can change this permission later in the Voice & Video section of Settings.", + "Allow Fallback": "Allow Fallback", + "Dismiss": "Dismiss", "Unable to capture screen": "Unable to capture screen", "Existing Call": "Existing Call", "You are already in a call.": "You are already in a call.", "VoIP is unsupported": "VoIP is unsupported", "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.", + "Homeserver not configured to support calls": "Homeserver not configured to support calls", "You cannot place a call with yourself.": "You cannot place a call with yourself.", "Could not connect to the integration server": "Could not connect to the integration server", "A conference call could not be started because the integrations server is not available": "A conference call could not be started because the integrations server is not available", @@ -94,7 +99,6 @@ "Unnamed Room": "Unnamed Room", "Error": "Error", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", - "Dismiss": "Dismiss", "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", "Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again", "Unable to enable Notifications": "Unable to enable Notifications", @@ -1508,10 +1512,6 @@ "Failed to leave room": "Failed to leave room", "Can't leave Server Notices room": "Can't leave Server Notices room", "This room is used for important messages from the Homeserver, so you cannot leave it.": "This room is used for important messages from the Homeserver, so you cannot leave it.", - "Homeserver not configured to support calls": "Homeserver not configured to support calls", - "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.": "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.", - "Riot can use a fallback server turn.matrix.org if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call. You can change this permission later in the Voice & Video section of Settings.": "Riot can use a fallback server turn.matrix.org if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call. You can change this permission later in the Voice & Video section of Settings.", - "Allow Fallback": "Allow Fallback", "Signed Out": "Signed Out", "For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.", "Terms and Conditions": "Terms and Conditions", From 67b830c48dd0c3720ba365bea21f7acc6491e9f6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 15 Aug 2019 11:11:46 +0100 Subject: [PATCH 116/413] Improve fallback ICE setting label --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ddf7a6baa7..8b6d7e2a32 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -359,7 +359,7 @@ "Show recently visited rooms above the room list": "Show recently visited rooms above the room list", "Show hidden events in timeline": "Show hidden events in timeline", "Low bandwidth mode": "Low bandwidth mode", - "Allow fallback call assist server turn.matrix.org": "Allow fallback call assist server turn.matrix.org", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 77e1c2cb25..b33ef3f8d7 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -374,7 +374,10 @@ export const SETTINGS = { }, "fallbackICEServerAllowed": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - displayName: _td("Allow fallback call assist server turn.matrix.org"), + displayName: _td( + "Allow fallback call assist server turn.matrix.org when your homeserver " + + "does not offer one (your IP address would be shared during a call)", + ), // This is a tri-state value, where `null` means "prompt the user". default: null, }, From d610bfc5e5d463b86a2b4db20f3f1d3030ea8f0d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 15 Aug 2019 14:44:50 +0100 Subject: [PATCH 117/413] Remove ICE fallback prompt from pre-call path --- src/CallHandler.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index a7feb74beb..595cf52c15 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -311,14 +311,6 @@ function _onAction(payload) { return; } - if ( - MatrixClientPeg.get().getTurnServers().length === 0 && - SettingsStore.getValue("fallbackICEServerAllowed") === null - ) { - _showICEFallbackPrompt(_t("Homeserver not configured to support calls")); - return; - } - const room = MatrixClientPeg.get().getRoom(payload.room_id); if (!room) { console.error("Room %s does not exist.", payload.room_id); From adc3c6902201b67b62c2884e062ba625c41c2441 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 15 Aug 2019 15:04:17 +0100 Subject: [PATCH 118/413] Update ICE fallback text --- src/CallHandler.js | 33 +++++++++++++++------------------ src/i18n/strings/en_EN.json | 12 ++++++------ 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 595cf52c15..f6b3e18538 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -151,7 +151,7 @@ function _setCallListeners(call) { MatrixClientPeg.get().getTurnServers().length === 0 && SettingsStore.getValue("fallbackICEServerAllowed") === null ) { - _showICEFallbackPrompt(_t("Call Failed")); + _showICEFallbackPrompt(); return; } @@ -225,32 +225,29 @@ function _setCallState(call, roomId, status) { }); } -function _showICEFallbackPrompt(title) { +function _showICEFallbackPrompt() { const cli = MatrixClientPeg.get(); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const code = sub => {sub}; Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { - title, + title: _t("Call failed due to misconfigured server"), description:
-

{ _t( - "Your homeserver %(homeserverDomain)s is " + - "currently not configured to assist with calls by offering a " + - "TURN server, which means it is likely that voice and video " + - "calls will fail. Please notify your homeserver administrator " + - "so that they can address this.", +

{_t( + "Please ask the administrator of your homeserver " + + "(%(homeserverDomain)s) to configure a TURN server in " + + "order for calls to work reliably.", { homeserverDomain: cli.getDomain() }, { code }, - ) }

-

{ _t( - "Riot can use a fallback server turn.matrix.org " + - "if you urgently need to make a call. Your IP address would be " + - "shared with this fallback server only if you agree and later " + - "place or receive a call. You can change this permission later " + - "in the Voice & Video section of Settings.", + )}

+

{_t( + "Alternatively, you can try to use the public server at " + + "turn.matrix.org, but this will not be as reliable, and " + + "it will share your IP address with that server. You can also manage " + + "this in Settings.", null, { code }, )}

, - button: _t('Allow Fallback'), - cancelButton: _t('Dismiss'), + button: _t('Try using turn.matrix.org'), + cancelButton: _t('OK'), onFinished: (allow) => { SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); cli.setFallbackICEServerAllowed(allow); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8b6d7e2a32..ce23180526 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -28,16 +28,16 @@ "Answer": "Answer", "Call Timeout": "Call Timeout", "The remote side failed to pick up": "The remote side failed to pick up", - "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.": "Your homeserver %(homeserverDomain)s is currently not configured to assist with calls by offering a TURN server, which means it is likely that voice and video calls will fail. Please notify your homeserver administrator so that they can address this.", - "Riot can use a fallback server turn.matrix.org if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call. You can change this permission later in the Voice & Video section of Settings.": "Riot can use a fallback server turn.matrix.org if you urgently need to make a call. Your IP address would be shared with this fallback server only if you agree and later place or receive a call. You can change this permission later in the Voice & Video section of Settings.", - "Allow Fallback": "Allow Fallback", - "Dismiss": "Dismiss", + "Call failed due to misconfigured server": "Call failed due to misconfigured server", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", + "Try using turn.matrix.org": "Try using turn.matrix.org", + "OK": "OK", "Unable to capture screen": "Unable to capture screen", "Existing Call": "Existing Call", "You are already in a call.": "You are already in a call.", "VoIP is unsupported": "VoIP is unsupported", "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.", - "Homeserver not configured to support calls": "Homeserver not configured to support calls", "You cannot place a call with yourself.": "You cannot place a call with yourself.", "Could not connect to the integration server": "Could not connect to the integration server", "A conference call could not be started because the integrations server is not available": "A conference call could not be started because the integrations server is not available", @@ -99,6 +99,7 @@ "Unnamed Room": "Unnamed Room", "Error": "Error", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", + "Dismiss": "Dismiss", "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", "Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again", "Unable to enable Notifications": "Unable to enable Notifications", @@ -383,7 +384,6 @@ "Decline": "Decline", "Accept": "Accept", "The other party cancelled the verification.": "The other party cancelled the verification.", - "OK": "OK", "Verified!": "Verified!", "You've successfully verified this user.": "You've successfully verified this user.", "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.", From 2bdce1229c0baab0e94433cfaff68d50b4bdb1ad Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Thu, 15 Aug 2019 10:19:20 +0000 Subject: [PATCH 119/413] Translated using Weblate (Dutch) Currently translated at 100.0% (1727 of 1727 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 42 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 4c3bb97118..5d047d5628 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1968,5 +1968,45 @@ "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Verkrijg opnieuw toegang tot uw account en herstel de versleutelingssleutels die opgeslagen zijn op dit apparaat. Zonder deze sleutels zult u uw versleutelde berichten niet kunnen lezen op andere apparaten.", "Sign in and regain access to your account.": "Meld u aan en verkrijg opnieuw toegang tot uw account.", "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt zich niet aanmelden met uw account. Neem contact op met de beheerder van uw thuisserver voor meer informatie.", - "This account has been deactivated.": "Deze account is gedeactiveerd." + "This account has been deactivated.": "Deze account is gedeactiveerd.", + "Failed to start chat": "Gesprek beginnen is mislukt", + "Messages": "Berichten", + "Actions": "Acties", + "Displays list of commands with usages and descriptions": "Toont een lijst van beschikbare opdrachten, met hun gebruiken en beschrijvingen", + "Identity Server URL must be HTTPS": "Identiteitsserver-URL moet HTTPS zijn", + "Not a valid Identity Server (status code %(code)s)": "Geen geldige identiteitsserver (statuscode %(code)s)", + "Could not connect to Identity Server": "Kon geen verbinding maken met de identiteitsserver", + "Checking server": "Server wordt gecontroleerd", + "Disconnect Identity Server": "Verbinding met identiteitsserver verbreken", + "Disconnect from the identity server ?": "Wilt u de verbinding met de identiteitsserver verbreken?", + "Disconnect": "Verbinding verbreken", + "Identity Server (%(server)s)": "Identiteitsserver (%(server)s)", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "U gebruikt momenteel om door uw contacten gevonden te kunnen worden, en om hen te kunnen vinden. U kunt hieronder uw identiteitsserver wijzigen.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "U gebruikt momenteel geen identiteitsserver. Voeg er hieronder één toe om door uw contacten gevonden te worden en om hen te kunnen vinden.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "De verbinding met uw identiteitsserver verbreken zal ertoe leiden dat u niet door andere gebruikers gevonden zal kunnen worden, en dat u anderen niet via e-mail of telefoon zal kunnen uitnodigen.", + "Integration manager offline or not accessible.": "Integratiebeheerder is offline of onbereikbaar.", + "Failed to update integration manager": "Bijwerken van integratiebeheerder is mislukt", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "U gebruikt momenteel %(serverName)s om uw bots, widgets en stickerpakketten te beheren.", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Voeg de integratiebeheerder toe die u wilt gebruiken om uw bots, widgets en stickerpakketten te beheren.", + "Integration Manager": "Integratiebeheerder", + "Enter a new integration manager": "Voer een nieuwe integratiebeheerder in", + "Discovery": "Ontdekking", + "Deactivate account": "Account deactiveren", + "Always show the window menu bar": "De venstermenubalk altijd tonen", + "Unable to revoke sharing for email address": "Kan delen voor dit e-mailadres niet intrekken", + "Unable to share email address": "Kan e-mailadres niet delen", + "Check your inbox, then click Continue": "Controleer uw postvak IN, en klik op Doorgaan", + "Revoke": "Intrekken", + "Share": "Delen", + "Discovery options will appear once you have added an email above.": "Ontdekkingsopties zullen verschijnen wanneer u een e-mailadres hebt toegevoegd.", + "Unable to revoke sharing for phone number": "Kan delen voor dit telefoonnummer niet intrekken", + "Unable to share phone number": "Kan telefoonnummer niet delen", + "Please enter verification code sent via text.": "Voer de verificatiecode in die werd verstuurd via sms.", + "Discovery options will appear once you have added a phone number above.": "Ontdekkingsopties zullen verschijnen wanneer u een telefoonnummer hebt toegevoegd.", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Er is een sms verstuurd naar +%(msisdn)s. Voor de verificatiecode in die in het bericht staat.", + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Controleer of de sleutel die u in uw Gebruikersinstellingen op dat apparaat ziet overeenkomt met de sleutel hieronder om te verifiëren dat het apparaat vertrouwd kan worden:", + "Command Help": "Hulp bij opdrachten", + "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "Er is geen identiteitsserver geconfigureerd, dus u kunt geen e-mailadres toevoegen om uw wachtwoord in de toekomst opnieuw in te stellen.", + "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "Er is geen identiteitsserver geconfigueerd: e-mailadressen kunnen niet worden toegevoegd. U zult uw wachtwoord niet opnieuw kunnen instellen.", + "No identity server is configured: add one in server settings to reset your password.": "Er is geen identiteitsserver geconfigureerd: voeg er één toe in de serverinstellingen om uw wachtwoord opnieuw in te stellen." } From c926e1a89fd59d6927f3aeb8ecaf6ec5882c71e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Thu, 15 Aug 2019 13:04:12 +0000 Subject: [PATCH 120/413] Translated using Weblate (French) Currently translated at 100.0% (1727 of 1727 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 951d8e864b..84b7fb5890 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2095,5 +2095,23 @@ "Command Help": "Aide aux commandes", "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "Aucun serveur d’identité n’est configuré donc vous ne pouvez pas ajouter d’adresse e-mail pour pouvoir réinitialiser votre mot de passe dans l’avenir.", "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "Aucun serveur d’identité n’est configuré : aucune adresse e-mail ne peut être ajoutée. Vous ne pourrez pas réinitialiser votre mot de passe.", - "No identity server is configured: add one in server settings to reset your password.": "Aucun serveur d’identité n’est configuré : ajoutez-en un dans les paramètres du serveur pour réinitialiser votre mot de passe." + "No identity server is configured: add one in server settings to reset your password.": "Aucun serveur d’identité n’est configuré : ajoutez-en un dans les paramètres du serveur pour réinitialiser votre mot de passe.", + "Identity Server URL must be HTTPS": "L’URL du serveur d’identité doit être en HTTPS", + "Not a valid Identity Server (status code %(code)s)": "Serveur d’identité non valide (code de statut %(code)s)", + "Could not connect to Identity Server": "Impossible de se connecter au serveur d’identité", + "Checking server": "Vérification du serveur", + "Disconnect Identity Server": "Déconnecter le serveur d’identité", + "Disconnect from the identity server ?": "Se déconnecter du serveur d’identité ?", + "Disconnect": "Se déconnecter", + "Identity Server (%(server)s)": "Serveur d’identité (%(server)s)", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Vous utilisez actuellement pour découvrir et être découvert par des contacts existants que vous connaissez. Vous pouvez changer votre serveur d’identité ci-dessous.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Vous n’utilisez actuellement aucun serveur d’identité. Pour découvrir et être découvert par les contacts existants que vous connaissez, ajoutez-en un ci-dessous.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "La déconnexion de votre serveur d’identité signifie que vous ne serez plus découvrable par d’autres utilisateurs et que vous ne pourrez plus faire d’invitation par e-mail ou téléphone.", + "Integration manager offline or not accessible.": "Gestionnaire d’intégration hors ligne ou inaccessible.", + "Failed to update integration manager": "Échec de la mise à jour du gestionnaire d’intégration", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "Vous utilisez actuellement %(serverName)s pour gérer vos robots, widgets et packs de stickers.", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Ajoutez quel gestionnaire d’intégration vous voulez pour gérer vos robots, widgets et packs de stickers.", + "Integration Manager": "Gestionnaire d’intégration", + "Enter a new integration manager": "Saisissez un nouveau gestionnaire d’intégration", + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Pour savoir si vous pouvez faire confiance à cet appareil, vérifiez que la clé que vous voyez dans les paramètres de l’utilisateur sur cet appareil correspond à la clé ci-dessous :" } From 002de2c691e0989d004b301f066d8d62be759500 Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Thu, 15 Aug 2019 10:31:24 +0000 Subject: [PATCH 121/413] Translated using Weblate (West Flemish) Currently translated at 100.0% (1727 of 1727 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/vls/ --- src/i18n/strings/vls.json | 44 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/vls.json b/src/i18n/strings/vls.json index 9051b4b991..ecec70b139 100644 --- a/src/i18n/strings/vls.json +++ b/src/i18n/strings/vls.json @@ -43,7 +43,7 @@ "You do not have permission to start a conference call in this room": "J’en èt geen toestemmienge voor in da groepsgesprek hier e vergoaderiengsgesprek te begunn", "Replying With Files": "Beantwoordn me bestandn", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Vo de moment es ’t nie meuglik van met e bestand te antwoordn. Wil je da bestand hier iploadn zounder te antwoordn?", - "Continue": "Deuregoan", + "Continue": "Verdergoan", "The file '%(fileName)s' failed to upload.": "’t Bestand ‘%(fileName)s’ kostege nie gipload wordn.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "’t Bestand ‘%(fileName)s’ es groter of den iploadlimiet van den thuusserver", "Upload Failed": "Iploadn mislukt", @@ -1710,5 +1710,45 @@ "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Je ku je nie anmeldn me jen account. Nimt contact ip me de beheerder van je thuusserver vo meer informoasje.", "You're signed out": "Je zyt afgemeld", "Clear personal data": "Persoonlike gegeevns wissn", - "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Let ip: je persoonlike gegeevns (inclusief versleuteriengssleuters) wordn nog alsan ip dit toestel ipgesloagn. Wist ze a je gereed zyt me ’t toestel te gebruukn, of a je je wilt anmeldn me nen andern account." + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Let ip: je persoonlike gegeevns (inclusief versleuteriengssleuters) wordn nog alsan ip dit toestel ipgesloagn. Wist ze a je gereed zyt me ’t toestel te gebruukn, of a je je wilt anmeldn me nen andern account.", + "Failed to start chat": "Gesprek beginn is mislukt", + "Messages": "Berichtn", + "Actions": "Acties", + "Displays list of commands with usages and descriptions": "Toogt e lyste van beschikboare ipdrachtn, met hunder gebruukn en beschryviengn", + "Identity Server URL must be HTTPS": "Den identiteitsserver-URL moet HTTPS zyn", + "Not a valid Identity Server (status code %(code)s)": "Geen geldigen identiteitsserver (statuscode %(code)s)", + "Could not connect to Identity Server": "Kostege geen verbindienge moakn me den identiteitsserver", + "Checking server": "Server wor gecontroleerd", + "Disconnect Identity Server": "Verbindienge me den identiteitsserver verbreekn", + "Disconnect from the identity server ?": "Wil je de verbindienge me den identiteitsserver verbreekn?", + "Disconnect": "Verbindienge verbreekn", + "Identity Server (%(server)s)": "Identiteitsserver (%(server)s)", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Je makt vo de moment gebruuk van vo deur je contactn gevoundn te kunn wordn, en von hunder te kunn viendn. Je kut hierounder jen identiteitsserver wyzign.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Je makt vo de moment geen gebruuk van een identiteitsserver. Voegt der hierounder één toe vo deur je contactn gevoundn te kunn wordn en von hunder te kunn viendn.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "De verbindienge me jen identiteitsserver verbreekn goat dervoorn zorgn da je nie mi deur andere gebruukers gevoundn goa kunn wordn, en dat andere menschn je nie via e-mail of telefong goan kunn uutnodign.", + "Integration manager offline or not accessible.": "D’integroasjebeheerder is offline of ounbereikboar.", + "Failed to update integration manager": "’t Bywerkn van d’integroasjebeheerder is mislukt", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "Je makt vo de moment gebruuk van %(serverName)s vo je bots, widgets en stickerpakketn te beheern.", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Voegt eender welken integroasjebeheerder toe vo je bots, widgets en stickerpakketn te beheern.", + "Integration Manager": "Integroasjebeheerder", + "Enter a new integration manager": "Voert e nieuwen integroasjebeheerder in", + "Discovery": "Ountdekkienge", + "Deactivate account": "Account deactiveern", + "Always show the window menu bar": "De veinstermenubalk alsan toogn", + "Unable to revoke sharing for email address": "Kostege ’t deeln vo dat e-mailadresse hier nie intrekkn", + "Unable to share email address": "Kostege ’t e-mailadresse nie deeln", + "Check your inbox, then click Continue": "Controleert je postvak IN, en klikt ip Verdergoan", + "Revoke": "Intrekkn", + "Share": "Deeln", + "Discovery options will appear once you have added an email above.": "Ountdekkiengsopties goan verschynn a j’een e-mailadresse toegevoegd ghed èt.", + "Unable to revoke sharing for phone number": "Kostege ’t deeln vo dien telefongnumero hier nie intrekkn", + "Unable to share phone number": "Kostege den telefongnumero nie deeln", + "Please enter verification code sent via text.": "Gift de verificoasjecode in da je in een smse gekreegn ghed èt.", + "Discovery options will appear once you have added a phone number above.": "Ountdekkiengsopties goan verschynn a j’e telefongnumero toegevoegd ghed èt.", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "’t Is een smse versteur noa +%(msisdn)s. Gift de verificoasjecode in da derin stoat.", + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Controleert of da de sleuter da j’in je Gebruukersinstelliengn ip da toestel ziet overeenkomt me de sleuter hierounder vo te verifieern da ’t toestel ku vertrouwd wordn:", + "Command Help": "Hulp by ipdrachtn", + "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "’t Is geen identiteitsserver geconfigureerd, dus je ku geen e-mailadresse toevoegn vo je paswoord in den toekomst herin te stelln.", + "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "’t Is geen identiteitsserver geconfigureerd gewist: e-mailadressn kunn nie toegevoegd wordn. Je goa je paswoord nie kunn herinstelln.", + "No identity server is configured: add one in server settings to reset your password.": "’t Is geen identiteitsserver geconfigureerd gewist: voegt der één toe in de serverinstelliengn vo je paswoord herin te stelln." } From 7d96cc969ada88e94147422707ce111f7b95428b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Aug 2019 15:39:15 +0100 Subject: [PATCH 122/413] use original bug Co-Authored-By: J. Ryan Stinnett --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 3a67d124ae..c0d103a219 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -166,7 +166,7 @@ export default class SetIdServer extends React.Component { "sharing them.", {}, { idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - // XXX: https://github.com/vector-im/riot-web/issues/10564 + // XXX: https://github.com/vector-im/riot-web/issues/9086 idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, }, ); From 27504e157863640e52b381e0da9406cafdac43bf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Aug 2019 13:28:23 -0600 Subject: [PATCH 123/413] Prompt for terms of service on integration manager changes Part of https://github.com/vector-im/riot-web/issues/10539 --- src/components/views/elements/Field.js | 12 +- .../views/settings/SetIntegrationManager.js | 113 ++++++++++++++---- src/i18n/strings/en_EN.json | 6 +- .../IntegrationManagerInstance.js | 9 +- src/integrations/IntegrationManagers.js | 19 +-- 5 files changed, 122 insertions(+), 37 deletions(-) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 8272b36639..43a0f8459c 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -46,6 +46,8 @@ export default class Field extends React.PureComponent { // and a `feedback` react component field to provide feedback // to the user. onValidate: PropTypes.func, + // If specified, overrides the value returned by onValidate. + flagInvalid: PropTypes.bool, // If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed. tooltipContent: PropTypes.node, @@ -137,7 +139,10 @@ export default class Field extends React.PureComponent { }, VALIDATION_THROTTLE_MS); render() { - const { element, prefix, onValidate, children, tooltipContent, ...inputProps } = this.props; + const { + element, prefix, onValidate, children, tooltipContent, + flagInvalid, ...inputProps, + } = this.props; const inputElement = element || "input"; @@ -157,13 +162,16 @@ export default class Field extends React.PureComponent { prefixContainer = {prefix}; } + const hasValidationFlag = flagInvalid != null && flagInvalid !== undefined; const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, { // If we have a prefix element, leave the label always at the top left and // don't animate it, as it looks a bit clunky and would add complexity to do // properly. mx_Field_labelAlwaysTopLeft: prefix, mx_Field_valid: onValidate && this.state.valid === true, - mx_Field_invalid: onValidate && this.state.valid === false, + mx_Field_invalid: hasValidationFlag + ? flagInvalid + : onValidate && this.state.valid === false, }); // Handle displaying feedback on validity diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index 8d26fdf40e..c6496beca7 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -19,6 +19,10 @@ import {_t} from "../../../languageHandler"; import sdk from '../../../index'; import Field from "../elements/Field"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import {SERVICE_TYPES} from "matrix-js-sdk"; +import {IntegrationManagerInstance} from "../../../integrations/IntegrationManagerInstance"; +import Modal from "../../../Modal"; export default class SetIntegrationManager extends React.Component { constructor() { @@ -31,6 +35,7 @@ export default class SetIntegrationManager extends React.Component { url: "", // user-entered text error: null, busy: false, + checking: false, }; } @@ -40,14 +45,14 @@ export default class SetIntegrationManager extends React.Component { }; _getTooltip = () => { - if (this.state.busy) { + if (this.state.checking) { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); return
{ _t("Checking server") }
; } else if (this.state.error) { - return this.state.error; + return {this.state.error}; } else { return null; } @@ -57,22 +62,7 @@ export default class SetIntegrationManager extends React.Component { return !!this.state.url && !this.state.busy; }; - _setManager = async (ev) => { - // Don't reload the page when the user hits enter in the form. - ev.preventDefault(); - ev.stopPropagation(); - - this.setState({busy: true}); - - const manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url); - if (!manager) { - this.setState({ - busy: false, - error: _t("Integration manager offline or not accessible."), - }); - return; - } - + _continueTerms = async (manager) => { try { await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager); this.setState({ @@ -90,6 +80,85 @@ export default class SetIntegrationManager extends React.Component { } }; + _setManager = async (ev) => { + // Don't reload the page when the user hits enter in the form. + ev.preventDefault(); + ev.stopPropagation(); + + this.setState({busy: true, checking: true, error: null}); + + let offline = false; + let manager: IntegrationManagerInstance; + try { + manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url); + offline = !manager; // no manager implies offline + } catch (e) { + console.error(e); + offline = true; // probably a connection error + } + if (offline) { + this.setState({ + busy: false, + checking: false, + error: _t("Integration manager offline or not accessible."), + }); + return; + } + + // Test the manager (causes terms of service prompt if agreement is needed) + // We also cancel the tooltip at this point so it doesn't collide with the dialog. + this.setState({checking: false}); + try { + const client = manager.getScalarClient(); + await client.connect(); + } catch (e) { + console.error(e); + this.setState({ + busy: false, + error: _t("Terms of service not accepted or the integration manager is invalid."), + }); + return; + } + + // Specifically request the terms of service to see if there are any. + // The above won't trigger a terms of service check if there are no terms to + // sign, so when there's no terms at all we need to ensure we tell the user. + let hasTerms = true; + try { + const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IM, manager.trimmedApiUrl); + hasTerms = terms && terms['policies'] && Object.keys(terms['policies']).length > 0; + } catch (e) { + // Assume errors mean there are no terms. This could be a 404, 500, etc + console.error(e); + hasTerms = false; + } + if (!hasTerms) { + this.setState({busy: false}); + const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); + Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { + title: _t("Integration manager has no terms of service"), + description: ( +
+ + {_t("The integration manager you have chosen does not have any terms of service.")} + + +  {_t("Only continue if you trust the owner of the server.")} + +
+ ), + button: _t("Continue"), + onFinished: async (confirmed) => { + if (!confirmed) return; + this._continueTerms(manager); + }, + }); + return; + } + + this._continueTerms(manager); + }; + render() { const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); @@ -120,11 +189,15 @@ export default class SetIntegrationManager extends React.Component { {bodyText} - %(serverName)s to manage your bots, widgets, and sticker packs.": "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.", "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.", "Integration Manager": "Integration Manager", diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index c21fff0fd3..6744d82e75 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -40,7 +40,14 @@ export class IntegrationManagerInstance { get name(): string { const parsed = url.parse(this.uiUrl); - return parsed.hostname; + return parsed.host; + } + + get trimmedApiUrl(): string { + const parsed = url.parse(this.apiUrl); + parsed.pathname = ''; + parsed.path = ''; + return parsed.format(); } getScalarClient(): ScalarAuthClient { diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 49356676e6..0e19c7add0 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -117,7 +117,8 @@ export class IntegrationManagers { } /** - * Attempts to discover an integration manager using only its name. + * Attempts to discover an integration manager using only its name. This will not validate that + * the integration manager is functional - that is the caller's responsibility. * @param {string} domainName The domain name to look up. * @returns {Promise} Resolves to an integration manager instance, * or null if none was found. @@ -153,20 +154,12 @@ export class IntegrationManagers { // All discovered managers are per-user managers const manager = new IntegrationManagerInstance(KIND_ACCOUNT, widget["data"]["api_url"], widget["url"]); - console.log("Got integration manager response, checking for responsiveness"); + console.log("Got an integration manager (untested)"); - // Test the manager - const client = manager.getScalarClient(); - try { - // not throwing an error is a success here - await client.connect(); - } catch (e) { - console.error(e); - console.warn("Integration manager failed liveliness check"); - return null; - } + // We don't test the manager because the caller may need to do extra + // checks or similar with it. For instance, they may need to deal with + // terms of service or want to call something particular. - console.log("Integration manager is alive and functioning"); return manager; } } From e2f013ddb4d66ea09da078eba9e9ce3a60b791f4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Aug 2019 13:31:45 -0600 Subject: [PATCH 124/413] Appease the linter This looks awkward, but should pass. --- src/components/views/elements/Field.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 43a0f8459c..9c248a81d8 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -140,9 +140,8 @@ export default class Field extends React.PureComponent { render() { const { - element, prefix, onValidate, children, tooltipContent, - flagInvalid, ...inputProps, - } = this.props; + element, prefix, onValidate, children, tooltipContent, flagInvalid, + ...inputProps} = this.props; const inputElement = element || "input"; From 5fe691cf339c0c92c8d4258eb0cfea8b10834805 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Aug 2019 13:33:02 -0600 Subject: [PATCH 125/413] Double equals --- src/components/views/elements/Field.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 9c248a81d8..2d9ef27edd 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -161,7 +161,7 @@ export default class Field extends React.PureComponent { prefixContainer = {prefix}; } - const hasValidationFlag = flagInvalid != null && flagInvalid !== undefined; + const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined; const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, { // If we have a prefix element, leave the label always at the top left and // don't animate it, as it looks a bit clunky and would add complexity to do From 9860baf0b4f9bffd708a7c2248ed2f54cf6f671d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Aug 2019 15:59:44 -0600 Subject: [PATCH 126/413] Prompt for terms of service on identity server changes Part of https://github.com/vector-im/riot-web/issues/10539 --- src/IdentityAuthClient.js | 51 +++++++++++++--- src/components/views/settings/SetIdServer.js | 63 +++++++++++++++++--- src/i18n/strings/en_EN.json | 5 +- 3 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 755205d5e2..12c0c5b147 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -14,15 +14,50 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { SERVICE_TYPES } from 'matrix-js-sdk'; +import Matrix, { SERVICE_TYPES } from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; export default class IdentityAuthClient { - constructor() { + /** + * Creates a new identity auth client + * @param {string} identityUrl The URL to contact the identity server with. + * When provided, this class will operate solely within memory, refusing to + * persist any information such as tokens. Default null (not provided). + */ + constructor(identityUrl = null) { this.accessToken = null; this.authEnabled = true; + + if (identityUrl) { + // XXX: We shouldn't have to create a whole new MatrixClient just to + // do identity server auth. The functions don't take an identity URL + // though, and making all of them take one could lead to developer + // confusion about what the idBaseUrl does on a client. Therefore, we + // just make a new client and live with it. + this.tempClient = Matrix.createClient({ + baseUrl: "", // invalid by design + idBaseUrl: identityUrl, + }); + } else { + // Indicates that we're using the real client, not some workaround. + this.tempClient = null; + } + } + + get _matrixClient() { + return this.tempClient ? this.tempClient : MatrixClientPeg.get(); + } + + _writeToken() { + if (this.tempClient) return; // temporary client: ignore + window.localStorage.setItem("mx_is_access_token", token); + } + + _readToken() { + if (this.tempClient) return null; // temporary client: ignore + return window.localStorage.getItem("mx_is_access_token"); } hasCredentials() { @@ -38,14 +73,14 @@ export default class IdentityAuthClient { let token = this.accessToken; if (!token) { - token = window.localStorage.getItem("mx_is_access_token"); + token = this._readToken(); } if (!token) { token = await this.registerForToken(); if (token) { this.accessToken = token; - window.localStorage.setItem("mx_is_access_token", token); + this._writeToken(); } return token; } @@ -61,7 +96,7 @@ export default class IdentityAuthClient { token = await this.registerForToken(); if (token) { this.accessToken = token; - window.localStorage.setItem("mx_is_access_token", token); + this._writeToken(); } } @@ -70,13 +105,13 @@ export default class IdentityAuthClient { async _checkToken(token) { try { - await MatrixClientPeg.get().getIdentityAccount(token); + await this._matrixClient.getIdentityAccount(token); } catch (e) { if (e.errcode === "M_TERMS_NOT_SIGNED") { console.log("Identity Server requires new terms to be agreed to"); await startTermsFlow([new Service( SERVICE_TYPES.IS, - MatrixClientPeg.get().idBaseUrl, + this._matrixClient.getIdentityServerUrl(), token, )]); return; @@ -95,7 +130,7 @@ export default class IdentityAuthClient { try { const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); const { access_token: identityAccessToken } = - await MatrixClientPeg.get().registerWithIdentityServer(hsOpenIdToken); + await this._matrixClient.registerWithIdentityServer(hsOpenIdToken); await this._checkToken(identityAccessToken); return identityAccessToken; } catch (e) { diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 398e578e8d..c4842d2ae9 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -22,6 +22,8 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import SdkConfig from "../../../SdkConfig"; import Modal from '../../../Modal'; import dis from "../../../dispatcher"; +import IdentityAuthClient from "../../../IdentityAuthClient"; +import {SERVICE_TYPES} from "matrix-js-sdk"; /** * If a url has no path component, etc. abbreviate it to just the hostname @@ -98,6 +100,7 @@ export default class SetIdServer extends React.Component { idServer: defaultIdServer, error: null, busy: false, + checking: false, }; } @@ -108,14 +111,14 @@ export default class SetIdServer extends React.Component { }; _getTooltip = () => { - if (this.state.busy) { + if (this.state.checking) { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); return
{ _t("Checking server") }
; } else if (this.state.error) { - return this.state.error; + return {this.state.error}; } else { return null; } @@ -125,25 +128,67 @@ export default class SetIdServer extends React.Component { return !!this.state.idServer && !this.state.busy; }; + _continueTerms = (fullUrl) => { + MatrixClientPeg.get().setIdentityServerUrl(fullUrl); + localStorage.removeItem("mx_is_access_token"); + localStorage.setItem("mx_is_url", fullUrl); + dis.dispatch({action: 'id_server_changed'}); + this.setState({idServer: '', busy: false, error: null}); + }; + _saveIdServer = async (e) => { e.preventDefault(); - this.setState({busy: true}); + this.setState({busy: true, checking: true, error: null}); const fullUrl = unabbreviateUrl(this.state.idServer); - const errStr = await checkIdentityServerUrl(fullUrl); + let errStr = await checkIdentityServerUrl(fullUrl); let newFormValue = this.state.idServer; if (!errStr) { - MatrixClientPeg.get().setIdentityServerUrl(fullUrl); - localStorage.removeItem("mx_is_access_token"); - localStorage.setItem("mx_is_url", fullUrl); - dis.dispatch({action: 'id_server_changed'}); - newFormValue = ''; + try { + this.setState({checking: false}); // clear tooltip + + // Test the identity server by trying to register with it. This + // may result in a terms of service prompt. + const authClient = new IdentityAuthClient(fullUrl); + await authClient.getAccessToken(); + + // Double check that the identity server even has terms of service. + const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); + if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { + const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); + Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { + title: _t("Identity server has no terms of service"), + description: ( +
+ + {_t("The identity server you have chosen does not have any terms of service.")} + + +  {_t("Only continue if you trust the owner of the server.")} + +
+ ), + button: _t("Continue"), + onFinished: async (confirmed) => { + if (!confirmed) return; + this._continueTerms(fullUrl); + }, + }); + return; + } + + this._continueTerms(fullUrl); + } catch (e) { + console.error(e); + errStr = _t("Terms of service not accepted or the identity server is invalid."); + } } this.setState({ busy: false, + checking: false, error: errStr, currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), idServer: newFormValue, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b982644516..09337fc7fc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -548,6 +548,10 @@ "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", "Could not connect to Identity Server": "Could not connect to Identity Server", "Checking server": "Checking server", + "Identity server has no terms of service": "Identity server has no terms of service", + "The identity server you have chosen does not have any terms of service.": "The identity server you have chosen does not have any terms of service.", + "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", + "Terms of service not accepted or the identity server is invalid.": "Terms of service not accepted or the identity server is invalid.", "Disconnect Identity Server": "Disconnect Identity Server", "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect": "Disconnect", @@ -562,7 +566,6 @@ "Terms of service not accepted or the integration manager is invalid.": "Terms of service not accepted or the integration manager is invalid.", "Integration manager has no terms of service": "Integration manager has no terms of service", "The integration manager you have chosen does not have any terms of service.": "The integration manager you have chosen does not have any terms of service.", - "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.", "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.", "Integration Manager": "Integration Manager", From 02f8b72533dbea1cbb0ed6cc4b80bb312b1f4f78 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Aug 2019 16:08:18 -0600 Subject: [PATCH 127/413] tfw the linter finds bugs for you --- src/IdentityAuthClient.js | 2 +- src/components/views/settings/SetIdServer.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 12c0c5b147..39785ef063 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -52,7 +52,7 @@ export default class IdentityAuthClient { _writeToken() { if (this.tempClient) return; // temporary client: ignore - window.localStorage.setItem("mx_is_access_token", token); + window.localStorage.setItem("mx_is_access_token", this.accessToken); } _readToken() { diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index c4842d2ae9..beea3f878f 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -144,8 +144,6 @@ export default class SetIdServer extends React.Component { const fullUrl = unabbreviateUrl(this.state.idServer); let errStr = await checkIdentityServerUrl(fullUrl); - - let newFormValue = this.state.idServer; if (!errStr) { try { this.setState({checking: false}); // clear tooltip @@ -191,7 +189,7 @@ export default class SetIdServer extends React.Component { checking: false, error: errStr, currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), - idServer: newFormValue, + idServer: this.state.idServer, }); }; From ae9039ab2c220a7a75c1d29f72248e038d6b1c43 Mon Sep 17 00:00:00 2001 From: Osoitz Date: Fri, 16 Aug 2019 09:46:54 +0000 Subject: [PATCH 128/413] Translated using Weblate (Basque) Currently translated at 98.4% (1705 of 1732 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 1323e6d258..3d46f2a60f 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -2021,5 +2021,24 @@ "To continue you need to accept the Terms of this service.": "Jarraitzeko zerbitzu honen baldintzak onartu behar dituzu.", "Service": "Zerbitzua", "Summary": "Laburpena", - "Terms": "Baldintzak" + "Terms": "Baldintzak", + "Call failed due to misconfigured server": "Deiak huts egin du zerbitzaria gaizki konfiguratuta dagoelako", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Eskatu zure hasiera-zerbitzariaren administratzaileari (%(homeserverDomain)s) TURN zerbitzari bat konfiguratu dezala deiek ondo funtzionatzeko.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Bestela, turn.matrix.org zerbitzari publikoa erabili dezakezu, baina hau ez dahorren fidagarria izango, eta zure IP-a partekatuko du zerbitzari horrekin. Hau ezarpenetan ere kudeatu dezakezu.", + "Try using turn.matrix.org": "Saiatu turn.matrix.org erabiltzen", + "Failed to start chat": "Huts egin du txata hastean", + "Messages": "Mezuak", + "Actions": "Ekintzak", + "Displays list of commands with usages and descriptions": "Aginduen zerrenda bistaratzen du, erabilera eta deskripzioekin", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Baimendu turn.matrix.org deien laguntzarako zerbitzaria erabiltzea zure hasiera-zerbitzariak bat eskaintzen ez duenean (Zure IP helbidea partekatuko da deian zehar)", + "Identity Server URL must be HTTPS": "Identitate zerbitzariaren URL-a HTTPS motakoa izan behar du", + "Not a valid Identity Server (status code %(code)s)": "Ez da identitate zerbitzari baliogarria (egoera-mezua %(code)s)", + "Could not connect to Identity Server": "Ezin izan da identitate-zerbitzarira konektatu", + "Checking server": "Zerbitzaria egiaztatzen", + "Disconnect Identity Server": "Identitate-zerbitzaritik deskonektatzen", + "Disconnect from the identity server ?": "Deskonektatu identitate-zerbitzaritik?", + "Disconnect": "Deskonektatu", + "Identity Server (%(server)s)": "Identitate-zerbitzaria (%(server)s)", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": " erabiltzen ari zara kontaktua aurkitzeko eta aurkigarria izateko. Zure identitate-zerbitzaria aldatu dezakezu azpian.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Orain ez duzu identitate-zerbitzaririk aurkitzen. Kontaktuak aurkitzeko eta aurkigarria izateko, gehitu bat azpian." } From 9c12791f37dadd99d53a8519045b4df99efbe916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Thu, 15 Aug 2019 20:40:01 +0000 Subject: [PATCH 129/413] Translated using Weblate (French) Currently translated at 100.0% (1732 of 1732 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 84b7fb5890..2f795f0cbd 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2113,5 +2113,10 @@ "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Ajoutez quel gestionnaire d’intégration vous voulez pour gérer vos robots, widgets et packs de stickers.", "Integration Manager": "Gestionnaire d’intégration", "Enter a new integration manager": "Saisissez un nouveau gestionnaire d’intégration", - "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Pour savoir si vous pouvez faire confiance à cet appareil, vérifiez que la clé que vous voyez dans les paramètres de l’utilisateur sur cet appareil correspond à la clé ci-dessous :" + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Pour savoir si vous pouvez faire confiance à cet appareil, vérifiez que la clé que vous voyez dans les paramètres de l’utilisateur sur cet appareil correspond à la clé ci-dessous :", + "Call failed due to misconfigured server": "Échec de l’appel à cause d’un serveur mal configuré", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Demandez à l’administrateur de votre serveur d’accueil (%(homeserverDomain)s) de configurer un serveur TURN afin que les appels fonctionnent de manière fiable.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Sinon, vous pouvez essayer d’utiliser le serveur public à turn.matrix.org, mais ça ne sera pas aussi fiable et ça partagera votre adresse IP avec ce serveur. Vous pouvez aussi gérer cela dans les paramètres.", + "Try using turn.matrix.org": "Essayer d’utiliser turn.matrix.org", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Autoriser le repli sur le serveur d’assistance d’appel turn.matrix.org quand votre serveur n’en fournit pas (votre adresse IP serait partagée lors d’un appel)" } From e705d110af3edb0656b8b000d8b3cb875acb40ea Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Aug 2019 11:57:32 +0100 Subject: [PATCH 130/413] Allow registering with email if no ID Server If the server advertises that it supports doing so This version uses a random me.dbkr prefix until the MSC is written. Requires https://github.com/matrix-org/matrix-js-sdk/pull/1017 Implements https://github.com/matrix-org/matrix-doc/pull/2233 --- .../structures/auth/Registration.js | 29 ++++++++++++++----- src/components/views/auth/RegistrationForm.js | 4 ++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 1c094cf862..0d3fe29967 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -98,6 +98,9 @@ module.exports = React.createClass({ // component without it. matrixClient: null, + // the capabilities object from the server + serverCaps: null, + // The user ID we've just registered registeredUsername: null, @@ -204,13 +207,24 @@ module.exports = React.createClass({ } const {hsUrl, isUrl} = serverConfig; - this.setState({ - matrixClient: Matrix.createClient({ - baseUrl: hsUrl, - idBaseUrl: isUrl, - }), + const cli = Matrix.createClient({ + baseUrl: hsUrl, + idBaseUrl: isUrl, + }); + + let caps = null; + try { + caps = await cli.getServerCapabilities(); + caps = caps || {}; + } catch (e) { + console.log("Unable to fetch server capabilities", e); + } + + this.setState({ + matrixClient: cli, + serverCaps: caps, + busy: false, }); - this.setState({busy: false}); try { await this._makeRegisterRequest({}); // This should never succeed since we specified an empty @@ -523,7 +537,7 @@ module.exports = React.createClass({ />; } else if (!this.state.matrixClient && !this.state.busy) { return null; - } else if (this.state.busy || !this.state.flows) { + } else if (this.state.busy || !this.state.flows | this.state.serverCaps === null) { return
; @@ -550,6 +564,7 @@ module.exports = React.createClass({ flows={this.state.flows} serverConfig={this.props.serverConfig} canSubmit={!this.state.serverErrorIsFatal} + serverCapabilities={this.state.serverCaps} />; } }, diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index f3b9640e16..368ab599e3 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -55,6 +55,7 @@ module.exports = React.createClass({ flows: PropTypes.arrayOf(PropTypes.object).isRequired, serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, canSubmit: PropTypes.bool, + serverCapabilities: PropTypes.object, }, getDefaultProps: function() { @@ -436,8 +437,9 @@ module.exports = React.createClass({ }, _showEmail() { + const idServerRequired = !this.props.serverCapabilities['me.dbkr.idomyownemail']; const haveIs = Boolean(this.props.serverConfig.isUrl); - if (!haveIs || !this._authStepIsUsed('m.login.email.identity')) { + if ((idServerRequired && !haveIs) || !this._authStepIsUsed('m.login.email.identity')) { return false; } return true; From 19c7a4627da348d28a608b4e5137abf3e03f592c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Aug 2019 12:24:52 +0100 Subject: [PATCH 131/413] fix test --- test/components/structures/auth/Registration-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.js index 6a8b35fbc0..38c7ae3849 100644 --- a/test/components/structures/auth/Registration-test.js +++ b/test/components/structures/auth/Registration-test.js @@ -69,9 +69,10 @@ describe('Registration', function() { const root = render(); - // Set non-empty flow & matrixClient to get past the loading spinner + // Set non-empty flows, capabilities & matrixClient to get past the loading spinner root.setState({ flows: [], + serverCaps: {}, matrixClient: {}, busy: false, }); From c90e4ab4a316858f3570c79fe878dbb7df9b8a88 Mon Sep 17 00:00:00 2001 From: Osoitz Date: Fri, 16 Aug 2019 09:57:15 +0000 Subject: [PATCH 132/413] Translated using Weblate (Basque) Currently translated at 100.0% (1733 of 1733 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 3d46f2a60f..5e39a3db87 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -2040,5 +2040,33 @@ "Disconnect": "Deskonektatu", "Identity Server (%(server)s)": "Identitate-zerbitzaria (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": " erabiltzen ari zara kontaktua aurkitzeko eta aurkigarria izateko. Zure identitate-zerbitzaria aldatu dezakezu azpian.", - "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Orain ez duzu identitate-zerbitzaririk aurkitzen. Kontaktuak aurkitzeko eta aurkigarria izateko, gehitu bat azpian." + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Orain ez duzu identitate-zerbitzaririk aurkitzen. Kontaktuak aurkitzeko eta aurkigarria izateko, gehitu bat azpian.", + "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "Orain zerbitzariarekin partekatzen dituzu e-mail helbideak edo telefono zenbakiak. zerbitzarira konektatu beharko zara partekatzeari uzteko.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Zure identitate-zerbitzaritik deskonektatzean ez zara beste erabiltzaileentzat aurkigarria izango eta ezin izango dituzu besteak gonbidatu e-mail helbidea edo telefono zenbakia erabiliz.", + "Integration manager offline or not accessible.": "Integrazio kudeatzailea lineaz kanpo edo ez eskuragarri.", + "Failed to update integration manager": "Huts egin du integrazio kudeatzailea eguneratzean", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "%(serverName)s erabiltzen ari zara zure botak, trepetak, eta eranskailu multzoak kudeatzeko.", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Ezarri zein integrazio-kudeatzailek kudeatuko dituen zure botak, trepetak, eta eranskailu-multzoak.", + "Integration Manager": "Integrazio-kudeatzailea", + "Enter a new integration manager": "Sartu integrazio-kudeatzaile berri bat", + "Discovery": "Aurkitzea", + "Deactivate account": "Desaktibatu kontua", + "Always show the window menu bar": "Erakutsi beti leihoaren menu barra", + "Unable to revoke sharing for email address": "Ezin izan da partekatzea indargabetu e-mail helbidearentzat", + "Unable to share email address": "Ezin izan da e-mail helbidea partekatu", + "Check your inbox, then click Continue": "Egiaztatu zure sarrera ontzia, gero sakatu jarraitu", + "Revoke": "Indargabetu", + "Share": "Partekatu", + "Discovery options will appear once you have added an email above.": "Aurkitze aukerak behin goian e-mail helbide bat gehitu duzunean agertuko dira.", + "Unable to revoke sharing for phone number": "Ezin izan da partekatzea indargabetu telefono zenbakiarentzat", + "Unable to share phone number": "Ezin izan da telefono zenbakia partekatu", + "Please enter verification code sent via text.": "Sartu SMS bidez bidalitako egiaztatze kodea.", + "Discovery options will appear once you have added a phone number above.": "Aurkitze aukerak behin goian telefono zenbaki bat bat gehitu duzunean agertuko dira.", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "SMS mezu bat bidali zaizu +%(msisdn)s zenbakira. Sartu hemen mezu horrek daukan egiaztatze-kodea.", + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Gailu hau fidagarria dela egiaztatzeko, egiaztatu gailu horretako Erabiltzaile ezarpenetan ikusi dezakezun gakoa beheko hau bera dela:", + "Command Help": "Aginduen laguntza", + "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "Ez da identitate-zerbitzaririk konfiguratu, beraz ezin duzu e-mail helbide bat gehitu zure pasahitza berrezartzeko etorkizunean.", + "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "Ez da identitate-zerbitzaririk konfiguratu, ezin da e-mail helbiderik gehitu. Ezin izango duzu zure pasahitza berrezarri.", + "No identity server is configured: add one in server settings to reset your password.": "Eza da identitate-zerbitzaririk konfiguratu, gehitu bat zerbitzari-ezarpenetan zure pasahitza berrezartzeko.", + "This account has been deactivated.": "Kontu hau desaktibatuta dago." } From c917463da84af85ec8ea0d49935ab5c54eaa29f6 Mon Sep 17 00:00:00 2001 From: albanobattistella Date: Fri, 16 Aug 2019 11:39:38 +0000 Subject: [PATCH 133/413] Translated using Weblate (Italian) Currently translated at 97.9% (1696 of 1733 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 51e7627da7..47aeb2d4da 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2022,5 +2022,17 @@ "To continue you need to accept the Terms of this service.": "Per continuare devi accettare le Condizioni di questo servizio.", "Service": "Servizio", "Summary": "Sommario", - "Terms": "Condizioni" + "Terms": "Condizioni", + "Call failed due to misconfigured server": "Chiamata non riuscita a causa di un server non configurato correttamente", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Chiedi all'amministratore del tuo homeserver(%(homeserverDomain)s) per configurare un server TURN affinché le chiamate funzionino in modo affidabile.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "In alternativa, puoi provare a utilizzare il server pubblico all'indirizzo turn.matrix.org, ma questo non sarà così affidabile e condividerà il tuo indirizzo IP con quel server. Puoi anche gestirlo in Impostazioni.", + "Try using turn.matrix.org": "Prova a usare turn.matrix.org", + "Failed to start chat": "Impossibile avviare la chat", + "Messages": "Messaggi", + "Actions": "Azioni", + "Displays list of commands with usages and descriptions": "Visualizza l'elenco dei comandi con usi e descrizioni", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Consenti al server di assistenza alle chiamate di fallback turn.matrix.org quando il tuo homeserver non ne offre uno (il tuo indirizzo IP verrà condiviso durante una chiamata)", + "Identity Server URL must be HTTPS": "L'URL di Identita' Server deve essere HTTPS", + "Not a valid Identity Server (status code %(code)s)": "Non è un server di identità valida (status code %(code)s)", + "Could not connect to Identity Server": "Impossibile connettersi ad in identita' Server" } From 41a9db32242ebf422bd8a725df08af4b9adbd2c1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Aug 2019 15:07:15 +0100 Subject: [PATCH 134/413] Use new flag in /versions --- src/components/structures/auth/Registration.js | 15 +++++++-------- src/components/views/auth/RegistrationForm.js | 5 ++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 0d3fe29967..af51ca7df0 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -98,8 +98,8 @@ module.exports = React.createClass({ // component without it. matrixClient: null, - // the capabilities object from the server - serverCaps: null, + // whether the HS requires an ID server to register with a threepid + serverRequiresIdServer: null, // The user ID we've just registered registeredUsername: null, @@ -212,17 +212,16 @@ module.exports = React.createClass({ idBaseUrl: isUrl, }); - let caps = null; + let serverRequiresIdServer = true; try { - caps = await cli.getServerCapabilities(); - caps = caps || {}; + serverRequiresIdServer = await cli.doesServerRequireIdServerParam(); } catch (e) { - console.log("Unable to fetch server capabilities", e); + console.log("Unable to determine is server needs id_server param", e); } this.setState({ matrixClient: cli, - serverCaps: caps, + serverRequiresIdServer, busy: false, }); try { @@ -564,7 +563,7 @@ module.exports = React.createClass({ flows={this.state.flows} serverConfig={this.props.serverConfig} canSubmit={!this.state.serverErrorIsFatal} - serverCapabilities={this.state.serverCaps} + serverRequiresIdServer={this.state.serverRequiresIdServer} />; } }, diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 368ab599e3..cf1b074fe1 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -55,7 +55,7 @@ module.exports = React.createClass({ flows: PropTypes.arrayOf(PropTypes.object).isRequired, serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, canSubmit: PropTypes.bool, - serverCapabilities: PropTypes.object, + serverRequiresIdServer: PropTypes.bool, }, getDefaultProps: function() { @@ -437,9 +437,8 @@ module.exports = React.createClass({ }, _showEmail() { - const idServerRequired = !this.props.serverCapabilities['me.dbkr.idomyownemail']; const haveIs = Boolean(this.props.serverConfig.isUrl); - if ((idServerRequired && !haveIs) || !this._authStepIsUsed('m.login.email.identity')) { + if ((this.props.serverRequiresIdServer && !haveIs) || !this._authStepIsUsed('m.login.email.identity')) { return false; } return true; From 3c4c595f7989a644f03502677ea02db090addb2d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Aug 2019 15:27:11 +0100 Subject: [PATCH 135/413] remove old serverCaps --- src/components/structures/auth/Registration.js | 2 +- test/components/structures/auth/Registration-test.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index af51ca7df0..63c5b267cf 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -536,7 +536,7 @@ module.exports = React.createClass({ />; } else if (!this.state.matrixClient && !this.state.busy) { return null; - } else if (this.state.busy || !this.state.flows | this.state.serverCaps === null) { + } else if (this.state.busy || !this.state.flows) { return
; diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.js index 38c7ae3849..3d4c8f921a 100644 --- a/test/components/structures/auth/Registration-test.js +++ b/test/components/structures/auth/Registration-test.js @@ -72,7 +72,6 @@ describe('Registration', function() { // Set non-empty flows, capabilities & matrixClient to get past the loading spinner root.setState({ flows: [], - serverCaps: {}, matrixClient: {}, busy: false, }); From a87fb7eaa26296d60d8ba8225dfc95af3a368ed3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Aug 2019 15:36:41 +0100 Subject: [PATCH 136/413] also remove from comment --- test/components/structures/auth/Registration-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.js index 3d4c8f921a..38188b2536 100644 --- a/test/components/structures/auth/Registration-test.js +++ b/test/components/structures/auth/Registration-test.js @@ -69,7 +69,7 @@ describe('Registration', function() { const root = render(); - // Set non-empty flows, capabilities & matrixClient to get past the loading spinner + // Set non-empty flows & matrixClient to get past the loading spinner root.setState({ flows: [], matrixClient: {}, From de3c489fbaa5f01ea98e7439ee15d69ffa9bca67 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 16 Aug 2019 09:41:23 -0600 Subject: [PATCH 137/413] Fix i18n --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8e49de48d3..587aa9d594 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -548,12 +548,12 @@ "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", "Could not connect to Identity Server": "Could not connect to Identity Server", "Checking server": "Checking server", - "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", - "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Identity server has no terms of service": "Identity server has no terms of service", "The identity server you have chosen does not have any terms of service.": "The identity server you have chosen does not have any terms of service.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", "Terms of service not accepted or the identity server is invalid.": "Terms of service not accepted or the identity server is invalid.", + "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", + "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect Identity Server": "Disconnect Identity Server", "Disconnect": "Disconnect", "Identity Server (%(server)s)": "Identity Server (%(server)s)", From dbe5c2cb452dc858d6cce9c08a61c0be4aeded41 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Aug 2019 18:11:24 +0100 Subject: [PATCH 138/413] Allow password reset without an ID Server If the server advertises that it supports doing so Requires matrix-org/matrix-js-sdk#1018 Requires matrix-org/matrix-js-sdk#1019 Fixes vector-im/riot-web#10572 --- src/PasswordReset.js | 6 +++++- src/components/structures/auth/ForgotPassword.js | 13 +++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/PasswordReset.js b/src/PasswordReset.js index df51e4d846..0dd5802962 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -36,7 +36,11 @@ class PasswordReset { idBaseUrl: identityUrl, }); this.clientSecret = this.client.generateClientSecret(); - this.identityServerDomain = identityUrl.split("://")[1]; + this.identityServerDomain = identityUrl ? identityUrl.split("://")[1] : null; + } + + doesServerRequireIdServerParam() { + return this.client.doesServerRequireIdServerParam(); } /** diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 0c1a8ec33d..6d80f66d64 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -62,10 +62,12 @@ module.exports = React.createClass({ serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", + serverRequiresIdServer: null, }; }, componentWillMount: function() { + this.reset = null; this._checkServerLiveliness(this.props.serverConfig); }, @@ -83,7 +85,14 @@ module.exports = React.createClass({ serverConfig.hsUrl, serverConfig.isUrl, ); - this.setState({serverIsAlive: true}); + + const pwReset = new PasswordReset(serverConfig.hsUrl, serverConfig.isUrl); + const serverRequiresIdServer = await pwReset.doesServerRequireIdServerParam(); + + this.setState({ + serverIsAlive: true, + serverRequiresIdServer, + }); } catch (e) { this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password")); } @@ -256,7 +265,7 @@ module.exports = React.createClass({ ; } - if (!this.props.serverConfig.isUrl) { + if (!this.props.serverConfig.isUrl && this.state.serverRequiresIdServer) { return

{yourMatrixAccountText} From 51946d2a744bfbf93c250e5abfbc0ec60e63d88b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 16 Aug 2019 13:10:41 -0600 Subject: [PATCH 139/413] Persist and maintain identity server in account data Fixes https://github.com/vector-im/riot-web/issues/10094 MSC: https://github.com/matrix-org/matrix-doc/pull/2230 --- src/components/structures/MatrixChat.js | 23 +++++++++++ src/components/views/settings/SetIdServer.js | 40 ++++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b8903076c7..aeffff9717 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -446,6 +446,29 @@ export default React.createClass({ } switch (payload.action) { + case 'MatrixActions.accountData': + // XXX: This is a collection of several hacks to solve a minor problem. We want to + // update our local state when the ID server changes, but don't want to put that in + // the js-sdk as we'd be then dictating how all consumers need to behave. However, + // this component is already bloated and we probably don't want this tiny logic in + // here, but there's no better place in the react-sdk for it. Additionally, we're + // abusing the MatrixActionCreator stuff to avoid errors on dispatches. + if (payload.event_type === 'm.identity_server') { + const fullUrl = payload.event_content ? payload.event_content['base_url'] : null; + if (!fullUrl) { + MatrixClientPeg.get().setIdentityServerUrl(null); + localStorage.removeItem("mx_is_access_token"); + localStorage.removeItem("mx_is_url"); + } else { + MatrixClientPeg.get().setIdentityServerUrl(fullUrl); + localStorage.removeItem("mx_is_access_token"); // clear token + localStorage.setItem("mx_is_url", fullUrl); // XXX: Do we still need this? + } + + // redispatch the change with a more specific action + dis.dispatch({action: 'id_server_changed'}); + } + break; case 'logout': Lifecycle.logout(); break; diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 096222f124..36ec9e8063 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -106,6 +106,31 @@ export default class SetIdServer extends React.Component { }; } + componentDidMount(): void { + this.dispatcherRef = dis.register(this.onAction); + } + + componentWillUnmount(): void { + dis.unregister(this.dispatcherRef); + } + + onAction = (payload) => { + // We react to changes in the ID server in the event the user is staring at this form + // when changing their identity server on another device. If the user is trying to change + // it in two places, we'll end up stomping all over their input, but at that point we + // should question our UX which led to them doing that. + if (payload.action !== "id_server_changed") return; + + const fullUrl = MatrixClientPeg.get().getIdentityServerUrl(); + let abbr = ''; + if (fullUrl) abbr = abbreviateUrl(fullUrl); + + this.setState({ + currentClientIdServer: fullUrl, + idServer: abbr, + }); + }; + _onIdentityServerChanged = (ev) => { const u = ev.target.value; @@ -131,10 +156,10 @@ export default class SetIdServer extends React.Component { }; _continueTerms = (fullUrl) => { - MatrixClientPeg.get().setIdentityServerUrl(fullUrl); - localStorage.removeItem("mx_is_access_token"); - localStorage.setItem("mx_is_url", fullUrl); - dis.dispatch({action: 'id_server_changed'}); + // Account data change will update localstorage, client, etc through dispatcher + MatrixClientPeg.get().setAccountData("m.identity_server", { + base_url: fullUrl, + }); this.setState({idServer: '', busy: false, error: null}); }; @@ -237,9 +262,10 @@ export default class SetIdServer extends React.Component { }; _disconnectIdServer = () => { - MatrixClientPeg.get().setIdentityServerUrl(null); - localStorage.removeItem("mx_is_access_token"); - localStorage.removeItem("mx_is_url"); + // Account data change will update localstorage, client, etc through dispatcher + MatrixClientPeg.get().setAccountData("m.identity_server", { + base_url: null, // clear + }); let newFieldVal = ''; if (SdkConfig.get()['validated_server_config']['isUrl']) { From 37a247613e25069a486efeac096a4d36dc6dd559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Sat, 17 Aug 2019 07:59:52 +0000 Subject: [PATCH 140/413] Translated using Weblate (French) Currently translated at 100.0% (1737 of 1737 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 2f795f0cbd..6cdecbfc9e 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2118,5 +2118,10 @@ "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Demandez à l’administrateur de votre serveur d’accueil (%(homeserverDomain)s) de configurer un serveur TURN afin que les appels fonctionnent de manière fiable.", "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Sinon, vous pouvez essayer d’utiliser le serveur public à turn.matrix.org, mais ça ne sera pas aussi fiable et ça partagera votre adresse IP avec ce serveur. Vous pouvez aussi gérer cela dans les paramètres.", "Try using turn.matrix.org": "Essayer d’utiliser turn.matrix.org", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Autoriser le repli sur le serveur d’assistance d’appel turn.matrix.org quand votre serveur n’en fournit pas (votre adresse IP serait partagée lors d’un appel)" + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Autoriser le repli sur le serveur d’assistance d’appel turn.matrix.org quand votre serveur n’en fournit pas (votre adresse IP serait partagée lors d’un appel)", + "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "Vous partagez actuellement vos adresses e-mail ou numéros de téléphone sur le serveur d’identité . Vous devrez vous reconnecter à pour arrêter de les partager.", + "Terms of service not accepted or the integration manager is invalid.": "Les conditions de service n’ont pas été acceptées ou le gestionnaire d’intégration n’est pas valide.", + "Integration manager has no terms of service": "Le gestionnaire d’intégration n’a pas de conditions de service", + "The integration manager you have chosen does not have any terms of service.": "Le gestionnaire d’intégration que vous avez choisi n’a aucune condition de service.", + "Only continue if you trust the owner of the server.": "Continuez seulement si vous faites confiance au propriétaire du serveur." } From 32a402322e849d6a9aeaf537f45e7b67176a8f96 Mon Sep 17 00:00:00 2001 From: MorbidMind Date: Sat, 17 Aug 2019 12:20:35 +0000 Subject: [PATCH 141/413] Translated using Weblate (Polish) Currently translated at 71.4% (1241 of 1737 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index d0d930f73d..08768a7794 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -138,7 +138,7 @@ "and %(count)s others...|other": "i %(count)s innych...", "and %(count)s others...|one": "i jeden inny...", "Bulk Options": "Masowe opcje", - "Call Timeout": "Upłynął limit czasu połączenia", + "Call Timeout": "Upłynął limit czasu łączenia", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Nie można nawiązać połączenia z serwerem - proszę sprawdź twoje połączenie, upewnij się, że certyfikat SSL serwera jest zaufany, i że dodatki przeglądarki nie blokują żądania.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Nie można nawiązać połączenia z serwerem przy użyciu HTTP podczas korzystania z HTTPS dla bieżącej strony. Użyj HTTPS lub włącz niebezpieczne skrypty.", "Can't load user settings": "Nie można załadować ustawień użytkownika", @@ -436,7 +436,7 @@ "The email address linked to your account must be entered.": "Musisz wpisać adres e-mail połączony z twoim kontem.", "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Rozmiar pliku '%(fileName)s' przekracza możliwy limit do przesłania na serwer domowy", "The file '%(fileName)s' failed to upload": "Przesyłanie pliku '%(fileName)s' nie powiodło się", - "The remote side failed to pick up": "Strona zdalna nie odebrała", + "The remote side failed to pick up": "Druga strona nie odebrała", "This room has no local addresses": "Ten pokój nie ma lokalnych adresów", "This room is not recognised.": "Ten pokój nie został rozpoznany.", "These are experimental features that may break in unexpected ways": "Te funkcje są eksperymentalne i może wystąpić błąd", @@ -674,14 +674,14 @@ "Which rooms would you like to add to this community?": "Które pokoje chcesz dodać do tej społeczności?", "Room name or alias": "Nazwa pokoju lub alias", "Add to community": "Dodaj do społeczności", - "Call": "Połącz", + "Call": "Zadzwoń", "Submit debug logs": "Wyślij dzienniki błędów", "The version of Riot.im": "Wersja Riot.im", "Whether or not you're logged in (we don't record your user name)": "Czy jesteś zalogowany, czy nie (nie zapisujemy twojej nazwy użytkownika)", "Your language of choice": "Twój wybrany język", - "Your homeserver's URL": "Adres URL twojego serwera domowego", - "Your identity server's URL": "Adres URL twojego serwera tożsamości", - "The information being sent to us to help make Riot.im better includes:": "Oto informacje przesyłane do nas, służące do poprawy Riot.im:", + "Your homeserver's URL": "Adres URL Twojego serwera domowego", + "Your identity server's URL": "Adres URL Twojego Serwera Tożsamości", + "The information being sent to us to help make Riot.im better includes:": "Informacje przesyłane do nas, by poprawić Riot.im zawierają:", "The platform you're on": "Platforma na której jesteś", "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "W tym pokoju są nieznane urządzenia: jeżeli będziesz kontynuować bez ich weryfikacji, możliwe będzie podsłuchiwanie Twojego połączenia.", "Answer": "Odbierz", @@ -959,7 +959,7 @@ "Which officially provided instance you are using, if any": "Jakiej oficjalnej instancji używasz, jeżeli w ogóle", "Every page you use in the app": "Każda strona, której używasz w aplikacji", "e.g. ": "np. ", - "Your User Agent": "Identyfikator Twojej przeglądarki", + "Your User Agent": "Identyfikator Twojej przeglądarki (User Agent)", "Your device resolution": "Twoja rozdzielczość ekranu", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Dane identyfikujące, takie jak: pokój, identyfikator użytkownika lub grupy, są usuwane przed wysłaniem na serwer.", "Who would you like to add to this community?": "Kogo chcesz dodać do tej społeczności?", @@ -1148,7 +1148,7 @@ "was kicked %(count)s times|one": "został wyrzucony", "Whether or not you're using the Richtext mode of the Rich Text Editor": "Niezależnie od tego, czy używasz trybu Richtext edytora tekstu w formacie RTF", "Call in Progress": "Łączenie w toku", - "Permission Required": "Wymagane Pozwolenie", + "Permission Required": "Wymagane Uprawnienia", "Registration Required": "Wymagana Rejestracja", "You need to register to do this. Would you like to register now?": "Musisz się zarejestrować, aby to zrobić. Czy chcesz się teraz zarejestrować?", "underlined": "podkreślenie", @@ -1158,8 +1158,8 @@ "block-quote": "blok cytowany", "A conference call could not be started because the intgrations server is not available": "Połączenie grupowe nie może zostać rozpoczęte, ponieważ serwer jest niedostępny", "A call is currently being placed!": "W tej chwili trwa rozmowa!", - "A call is already in progress!": "W tej chwili trwa połączenie!", - "You do not have permission to start a conference call in this room": "Nie posiadasz permisji do rozpoczęcia rozmowy grupowej w tym pokoju", + "A call is already in progress!": "Połączenie już trwa!", + "You do not have permission to start a conference call in this room": "Nie posiadasz uprawnień do rozpoczęcia rozmowy grupowej w tym pokoju", "Unignored user": "Nieignorowany użytkownik", "Forces the current outbound group session in an encrypted room to be discarded": "Wymusza odrzucenie bieżącej sesji grupy wychodzącej w zaszyfrowanym pokoju", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s dodał(a) %(addedAddresses)s jako adres tego pokoju.", @@ -1479,5 +1479,18 @@ "Browse": "Przeglądaj", "Once enabled, encryption cannot be disabled.": "Po włączeniu szyfrowanie nie może zostać wyłączone.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Aby uniknąć duplikowania problemów, prosimy najpierw przejrzeć istniejące problemy (i dodać +1) lub utworzyć nowy problem, jeżeli nie możesz go znaleźć.", - "Go back": "Wróć" + "Go back": "Wróć", + "Whether or not you're logged in (we don't record your username)": "Niezależnie od tego, czy jesteś zalogowany (nie zapisujemy Twojej nazwy użytkownika)", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Niezależnie od tego, czy korzystasz z funkcji \"okruchy\" (awatary nad listą pokoi)", + "Call failed due to misconfigured server": "Połączenie nie udało się przez błędną konfigurację serwera", + "Try using turn.matrix.org": "Spróbuj użyć serwera turn.matrix.org", + "A conference call could not be started because the integrations server is not available": "Połączenie konferencyjne nie może zostać rozpoczęte ponieważ serwer integracji jest niedostępny", + "The file '%(fileName)s' failed to upload.": "Nie udało się przesłać pliku '%(fileName)s'.", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Plik '%(fileName)s' przekracza limit rozmiaru dla tego serwera głównego", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Poproś swojego administratora Riot by sprawdzić Twoją konfigurację względem niewłaściwych lub zduplikowanych elementów.", + "Cannot reach identity server": "Nie można połączyć się z Serwerem Tożsamości", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Możesz się zarejestrować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Możesz zresetować hasło, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Możesz się zalogować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", + "No homeserver URL provided": "Nie podano URL serwera głównego." } From f32c4de1f5bfb075d8e908f89e7b7822a411cfc0 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 19 Aug 2019 06:47:47 +0000 Subject: [PATCH 142/413] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1737 of 1737 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 51989395ad..71da1fa9dd 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2106,5 +2106,15 @@ "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "新增用來管理您的機器人、小工具與貼紙包的整合管理員。", "Integration Manager": "整合管理員", "Enter a new integration manager": "輸入新的整合管理員", - "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "要驗證此裝置是否可受信任,請檢查您在裝置上的使用者設定裡看到的金鑰是否與下方的金鑰相同:" + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "要驗證此裝置是否可受信任,請檢查您在裝置上的使用者設定裡看到的金鑰是否與下方的金鑰相同:", + "Call failed due to misconfigured server": "因為伺服器設定錯誤,所以通話失敗", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "請詢問您家伺服器的管理員(%(homeserverDomain)s)以設定 TURN 伺服器讓通話可以正常運作。", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "或是您也可以試著使用公開伺服器 turn.matrix.org,但可能不夠可靠,而且會跟該伺服器分享您的 IP 位置。您也可以在設定中管理這個。", + "Try using turn.matrix.org": "嘗試使用 turn.matrix.org", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "當您的家伺服器不提供這類服務時,將 turn.matrix.org 新增為備用伺服器(您的 IP 位置將會在通話期間被分享)", + "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "您目前在身份識別伺服器 上分享電子郵件地址或電話號碼。您將必須重新連線到 以停止分享它們。", + "Terms of service not accepted or the integration manager is invalid.": "服務條款不接受或整合管理員無效。", + "Integration manager has no terms of service": "整合管理員沒有服務條款", + "The integration manager you have chosen does not have any terms of service.": "您選擇的整合管理員沒有任何服務條款。", + "Only continue if you trust the owner of the server.": "僅在您信任伺服器擁有者時才繼續。" } From f13033516935fe1246408d696fdf59ac7c257f8b Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Mon, 19 Aug 2019 08:23:32 +0000 Subject: [PATCH 143/413] Translated using Weblate (Finnish) Currently translated at 98.6% (1712 of 1737 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index a10b978082..17a696df9b 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -1976,5 +1976,13 @@ "Identity Server (%(server)s)": "Identiteettipalvelin (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Käytät palvelinta tuntemiesi henkilöiden löytämiseen ja löydetyksi tulemiseen. Voit vaihtaa identiteettipalvelintasi alla.", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Et käytä tällä hetkellä identiteettipalvelinta. Lisää identiteettipalvelin alle löytääksesi tuntemiasi henkilöitä ja tullaksesi löydetyksi.", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Yhteyden katkaiseminen identiteettipalvelimeesi tarkoittaa, että muut käyttäjät eivät löydä sinua etkä voi kutsua muita sähköpostin tai puhelinnumeron perusteella." + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Yhteyden katkaiseminen identiteettipalvelimeesi tarkoittaa, että muut käyttäjät eivät löydä sinua etkä voi kutsua muita sähköpostin tai puhelinnumeron perusteella.", + "Call failed due to misconfigured server": "Puhelu epäonnistui palvelimen väärien asetusten takia", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Pyydä kotipalvelimesi (%(homeserverDomain)s) ylläpitäjää asentamaan TURN-palvelin, jotta puhelut toimisivat luotettavasti.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Vaihtoehtoisesti voit kokeilla käyttää julkista palvelinta osoitteessa turn.matrix.org, mutta tämä vaihtoehto ei ole yhtä luotettava ja jakaa IP-osoitteesi palvelimen kanssa. Voit myös hallita tätä asiaa asetuksissa.", + "Try using turn.matrix.org": "Kokeile käyttää palvelinta turn.matrix.org", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Salli varalle puhelujen apupalvelin turn.matrix.org kun kotipalvelimesi ei tarjoa sellaista (IP-osoitteesi jaetaan puhelun aikana)", + "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "Jaat tällä hetkellä sähköpostiosoitteita tai puhelinnumeroja identiteettipalvelimella . Sinun täytyy yhdistää uudelleen palvelimelle lopettaaksesi niiden jakamisen.", + "Only continue if you trust the owner of the server.": "Jatka vain, jos luotat palvelimen omistajaan.", + "reacted with %(shortName)s": "reagoi(vat) emojilla %(shortName)s" } From 07826c567546f79901534e65ac3f9668361c2693 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 19 Aug 2019 14:03:38 +0100 Subject: [PATCH 144/413] Hide 3PID discovery sections when no identity server This hides the email and phone sections of Discovery in the Settings when there is no IS, as they can't meaningfully be used. Part of https://github.com/vector-im/riot-web/issues/10528 --- .../settings/tabs/user/GeneralUserSettingsTab.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 08550db1d1..b428ef51fa 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -189,13 +189,17 @@ export default class GeneralUserSettingsTab extends React.Component { const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers"); const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); + const threepidSection = this.state.haveIdServer ?
+ {_t("Email addresses")} + + + {_t("Phone numbers")} + +
: null; + return (
- {_t("Email addresses")} - - - {_t("Phone numbers")} - + {threepidSection} { /* has its own heading as it includes the current ID server */ }
From d4ecb99d11f8725711d9cc648e11b9b7283806fe Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 19 Aug 2019 14:20:01 +0100 Subject: [PATCH 145/413] Show the default IS as a placeholder in Settings This changes the UX for the set IS field to show the default IS as a placeholder value (as opposed to an initial value as if the user had actually entered it). Fixes https://github.com/vector-im/riot-web/issues/10528 --- src/components/views/settings/SetIdServer.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index c0d103a219..8b9c3150a3 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -95,8 +95,9 @@ export default class SetIdServer extends React.Component { } this.state = { + defaultIdServer, currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), - idServer: defaultIdServer, + idServer: "", error: null, busy: false, disconnectBusy: false, @@ -265,7 +266,10 @@ export default class SetIdServer extends React.Component { From 7cc27beb4433ea78a95a56dc13b3eb0e8be1caee Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 19 Aug 2019 14:34:22 +0100 Subject: [PATCH 146/413] Remove custom font size for IM URL As best as I can tell, the designs call for the IM URL to be the same size as other subheadings. The font size in Zeplin doesn't match what is currently used in Settings, so this likely caused confusion. --- res/css/views/settings/_SetIntegrationManager.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss index 7fda042864..d532492ea8 100644 --- a/res/css/views/settings/_SetIntegrationManager.scss +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -30,5 +30,4 @@ limitations under the License. .mx_SetIntegrationManager > .mx_SettingsTab_heading > .mx_SettingsTab_subheading { display: inline-block; padding-left: 5px; - font-size: 14px; } From 5af9bf7b8035dd086e797dbf40ea735f4c67ee2e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 19 Aug 2019 14:44:53 +0100 Subject: [PATCH 147/413] Use designed text for new IS field --- src/components/views/settings/SetIdServer.js | 2 +- src/i18n/strings/en_EN.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index c0d103a219..38e8f3dd51 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -263,7 +263,7 @@ export default class SetIdServer extends React.Component { {bodyText} - Date: Mon, 19 Aug 2019 15:17:14 +0100 Subject: [PATCH 148/413] Allow 3pids to be added with no ID server set Fixes https://github.com/vector-im/riot-web/issues/10573 --- .../tabs/user/GeneralUserSettingsTab.js | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index b428ef51fa..7a8d123fcd 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -46,11 +46,17 @@ export default class GeneralUserSettingsTab extends React.Component { language: languageHandler.getCurrentLanguage(), theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), + serverRequiresIdServer: null, }; this.dispatcherRef = dis.register(this._onAction); } + async componentWillMount() { + const serverRequiresIdServer = await MatrixClientPeg.get().doesServerRequireIdServerParam(); + this.setState({serverRequiresIdServer}); + } + componentWillUnmount() { dis.unregister(this.dispatcherRef); } @@ -127,6 +133,7 @@ export default class GeneralUserSettingsTab extends React.Component { const ChangePassword = sdk.getComponent("views.settings.ChangePassword"); const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses"); const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers"); + const Spinner = sdk.getComponent("views.elements.Spinner"); const passwordChangeForm = ( ); - const threepidSection = this.state.haveIdServer ?
- {_t("Email addresses")} - + let threepidSection = null; - {_t("Phone numbers")} - -
: null; + if (this.state.haveIdServer || this.state.serverRequiresIdServer === false) { + threepidSection =
+ {_t("Email addresses")} + + + {_t("Phone numbers")} + +
; + } else if (this.state.serverRequiresIdServer === null) { + threepidSection = ; + } return (
From 93af6cfd8d01cc028a956505cc13289059af25f2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 19 Aug 2019 15:37:12 +0100 Subject: [PATCH 149/413] Fix up remove threepid confirmation UX Probably still not the best design but hopefully break fewer UX rules: 1. Use red to confirm delete rather than cancel and green to cancel 2. Show the action you're about to perform in the confirmation 3. Label confirmation button with the action rather than yes/no. --- .../views/settings/account/EmailAddresses.js | 10 +++++----- src/components/views/settings/account/PhoneNumbers.js | 10 +++++----- src/i18n/strings/en_EN.json | 8 +++++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js index c13d2b4e0f..eb60d4a322 100644 --- a/src/components/views/settings/account/EmailAddresses.js +++ b/src/components/views/settings/account/EmailAddresses.js @@ -87,15 +87,15 @@ export class ExistingEmailAddress extends React.Component { return (
- {_t("Are you sure?")} + {_t("Remove %(email)s?", {email: this.props.email.address} )} - - {_t("Yes")} + {_t("Remove")} - - {_t("No")} + {_t("Cancel")}
); diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js index 236a4e7587..fbb5b7e561 100644 --- a/src/components/views/settings/account/PhoneNumbers.js +++ b/src/components/views/settings/account/PhoneNumbers.js @@ -82,15 +82,15 @@ export class ExistingPhoneNumber extends React.Component { return (
- {_t("Are you sure?")} + {_t("Remove %(phone)s?", {phone: this.props.msisdn.address})} - - {_t("Yes")} + {_t("Remove")} - - {_t("No")} + {_t("Cancel")}
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 89d9026fcf..b93d1e1ee2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -721,9 +721,7 @@ "Verification code": "Verification code", "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", "Unable to remove contact information": "Unable to remove contact information", - "Are you sure?": "Are you sure?", - "Yes": "Yes", - "No": "No", + "Remove %(email)s?": "Remove %(email)s?", "Remove": "Remove", "Invalid Email Address": "Invalid Email Address", "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", @@ -731,6 +729,7 @@ "Add": "Add", "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.", "Email Address": "Email Address", + "Remove %(phone)s?": "Remove %(phone)s?", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.", "Phone Number": "Phone Number", "Cannot add any more widgets": "Cannot add any more widgets", @@ -777,6 +776,7 @@ "Failed to toggle moderator status": "Failed to toggle moderator status", "Failed to change power level": "Failed to change power level", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", + "Are you sure?": "Are you sure?", "No devices with registered encryption keys": "No devices with registered encryption keys", "Ignore": "Ignore", "Jump to read receipt": "Jump to read receipt", @@ -1069,6 +1069,8 @@ "Verify...": "Verify...", "Join": "Join", "No results": "No results", + "Yes": "Yes", + "No": "No", "Communities": "Communities", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)", "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s", From 8a7b43fd1ad86d5b5e7b843938ffabf58f490b66 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 19 Aug 2019 15:45:04 +0100 Subject: [PATCH 150/413] Tweak Settings padding / margin slightly This makes a few small tweaks to Settings padding and margin, but more is still needed. I am told that https://github.com/vector-im/riot-web/issues/10554 will soon take a holistic view on the problem, so this is a more conservation temporary change. --- res/css/views/settings/tabs/_SettingsTab.scss | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 7755ee6053..84ff91d341 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -24,6 +24,10 @@ limitations under the License. color: $primary-fg-color; } +.mx_SettingsTab_heading:nth-child(n + 2) { + margin-top: 30px; +} + .mx_SettingsTab_subheading { font-size: 16px; display: block; @@ -37,9 +41,8 @@ limitations under the License. .mx_SettingsTab_subsectionText { color: $settings-subsection-fg-color; font-size: 14px; - padding-bottom: 12px; display: block; - margin: 0 100px 0 0; // Align with the rest of the view + margin: 10px 100px 10px 0; // Align with the rest of the view } .mx_SettingsTab_section .mx_SettingsFlag { @@ -68,9 +71,14 @@ limitations under the License. } .mx_SettingsTab .mx_SettingsTab_subheading:nth-child(n + 2) { + // TODO: This `nth-child(n + 2)` isn't working very well since many sections + // add intermediate elements (mostly because our version of React requires + // them) which throws off the numbering and results in many subheading + // missing margins. + // See also https://github.com/vector-im/riot-web/issues/10554 // These views have a lot of the same repetitive information on it, so // give them more visual distinction between the sections. - margin-top: 30px; + margin-top: 25px; } .mx_SettingsTab a { From 32abfbbfc60642e1b6cdb5308518a557a88a15e7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 10:27:57 -0600 Subject: [PATCH 151/413] Rename functions --- src/components/views/settings/SetIdServer.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 096222f124..67ca11d04f 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -130,7 +130,7 @@ export default class SetIdServer extends React.Component { return !!this.state.idServer && !this.state.busy; }; - _continueTerms = (fullUrl) => { + _saveIdServer = (fullUrl) => { MatrixClientPeg.get().setIdentityServerUrl(fullUrl); localStorage.removeItem("mx_is_access_token"); localStorage.setItem("mx_is_url", fullUrl); @@ -138,7 +138,7 @@ export default class SetIdServer extends React.Component { this.setState({idServer: '', busy: false, error: null}); }; - _saveIdServer = async (e) => { + _checkIdServer = async (e) => { e.preventDefault(); this.setState({busy: true, checking: true, error: null}); @@ -174,13 +174,13 @@ export default class SetIdServer extends React.Component { button: _t("Continue"), onFinished: async (confirmed) => { if (!confirmed) return; - this._continueTerms(fullUrl); + this._saveIdServer(fullUrl); }, }); return; } - this._continueTerms(fullUrl); + this._saveIdServer(fullUrl); } catch (e) { console.error(e); errStr = _t("Terms of service not accepted or the identity server is invalid."); @@ -299,7 +299,7 @@ export default class SetIdServer extends React.Component { } return ( - + {sectionTitle} @@ -313,7 +313,7 @@ export default class SetIdServer extends React.Component { tooltipContent={this._getTooltip()} /> {_t("Change")} {discoSection} From 855bf3ad0dd96fcb5a734b7ac1696e862efc2bdc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 10:25:20 -0600 Subject: [PATCH 152/413] Import createClient instead --- src/IdentityAuthClient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 39785ef063..d3b4d8a6de 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Matrix, { SERVICE_TYPES } from 'matrix-js-sdk'; +import { createClient, SERVICE_TYPES } from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; @@ -36,7 +36,7 @@ export default class IdentityAuthClient { // though, and making all of them take one could lead to developer // confusion about what the idBaseUrl does on a client. Therefore, we // just make a new client and live with it. - this.tempClient = Matrix.createClient({ + this.tempClient = createClient({ baseUrl: "", // invalid by design idBaseUrl: identityUrl, }); From f7083ac332ff3e45f1ad917b6c42930f57bca2f6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 19 Aug 2019 18:09:37 +0100 Subject: [PATCH 153/413] Clarify that device names are publicly visible And also a bunch of other UI fixes in the devices table: * It's the devices table, don't need 'device' in all the headers * Not really necessary to label checkboxes with 'select' * Stop table from moving down when the delete button appears Fixes https://github.com/vector-im/riot-web/issues/10216 --- res/css/views/settings/_DevicesPanel.scss | 5 +++++ src/components/views/settings/DevicesPanel.js | 10 +++++----- .../settings/tabs/user/SecurityUserSettingsTab.js | 1 + src/i18n/strings/en_EN.json | 7 ++++--- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/res/css/views/settings/_DevicesPanel.scss b/res/css/views/settings/_DevicesPanel.scss index 4113fc4ebc..581ff47fc1 100644 --- a/res/css/views/settings/_DevicesPanel.scss +++ b/res/css/views/settings/_DevicesPanel.scss @@ -26,8 +26,13 @@ limitations under the License. font-weight: bold; } +.mx_DevicesPanel_header > .mx_DevicesPanel_deviceButtons { + height: 48px; // make this tall so the table doesn't move down when the delete button appears +} + .mx_DevicesPanel_header > div { display: table-cell; + vertical-align: bottom; } .mx_DevicesPanel_header .mx_DevicesPanel_deviceLastSeen { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index 1f502fac2f..30f507ea18 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket 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. @@ -186,7 +187,7 @@ export default class DevicesPanel extends React.Component { const deleteButton = this.state.deleting ? : - + { _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) } ; @@ -194,11 +195,11 @@ export default class DevicesPanel extends React.Component { return (
-
{ _t("Device ID") }
-
{ _t("Device Name") }
+
{ _t("ID") }
+
{ _t("Public Name") }
{ _t("Last seen") }
- { this.state.selectedDevices.length > 0 ? deleteButton : _t('Select devices') } + { this.state.selectedDevices.length > 0 ? deleteButton : null }
{ devices.map(this._renderDevice) } @@ -207,7 +208,6 @@ export default class DevicesPanel extends React.Component { } } -DevicesPanel.displayName = 'MemberDeviceInfo'; DevicesPanel.propTypes = { className: PropTypes.string, }; diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index c18f5bda53..e619791b01 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -258,6 +258,7 @@ export default class SecurityUserSettingsTab extends React.Component {
{_t("Devices")}
+ {_t("A device's public name is visible to people you communicate with")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b93d1e1ee2..0c2e018c77 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -476,10 +476,9 @@ "Authentication": "Authentication", "Delete %(count)s devices|other": "Delete %(count)s devices", "Delete %(count)s devices|one": "Delete device", - "Device ID": "Device ID", - "Device Name": "Device Name", + "ID": "ID", + "Public Name": "Public Name", "Last seen": "Last seen", - "Select devices": "Select devices", "Failed to set display name": "Failed to set display name", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", @@ -628,6 +627,7 @@ "Key backup": "Key backup", "Security & Privacy": "Security & Privacy", "Devices": "Devices", + "A device's public name is visible to people you communicate with": "A device's public name is visible to people you communicate with", "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", "Learn more about how we use analytics.": "Learn more about how we use analytics.", @@ -1211,6 +1211,7 @@ "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:", "Use two-way text verification": "Use two-way text verification", "Device name": "Device name", + "Device ID": "Device ID", "Device key": "Device key", "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.", "I verify that the keys match": "I verify that the keys match", From 83af732d05d44b8c6389775c5616833bfc375dfb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 22:53:37 -0600 Subject: [PATCH 154/413] Rename and export abbreviateIdentityUrl --- src/components/views/settings/SetIdServer.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index c5e979da04..135d3c32fd 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -32,14 +32,14 @@ import {SERVICE_TYPES} from "matrix-js-sdk"; * @param {string} u The url to be abbreviated * @returns {string} The abbreviated url */ -function abbreviateUrl(u) { +export function abbreviateIdentityUrl(u) { if (!u) return ''; const parsedUrl = url.parse(u); // if it's something we can't parse as a url then just return it if (!parsedUrl) return u; - if (parsedUrl.path == '/') { + if (parsedUrl.path === '/') { // we ignore query / hash parts: these aren't relevant for IS server URLs return parsedUrl.host; } @@ -93,7 +93,7 @@ export default class SetIdServer extends React.Component { if (!MatrixClientPeg.get().getIdentityServerUrl() && SdkConfig.get()['validated_server_config']['isUrl']) { // If no ID server is configured but there's one in the config, prepopulate // the field to help the user. - defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); + defaultIdServer = abbreviateIdentityUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.state = { @@ -124,7 +124,7 @@ export default class SetIdServer extends React.Component { const fullUrl = MatrixClientPeg.get().getIdentityServerUrl(); let abbr = ''; - if (fullUrl) abbr = abbreviateUrl(fullUrl); + if (fullUrl) abbr = abbreviateIdentityUrl(fullUrl); this.setState({ currentClientIdServer: fullUrl, @@ -234,15 +234,15 @@ export default class SetIdServer extends React.Component { "server . You will need to reconnect to to stop " + "sharing them.", {}, { - idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + idserver: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}, // XXX: https://github.com/vector-im/riot-web/issues/9086 - idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + idserver2: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}, }, ); } else { message = _t( "Disconnect from the identity server ?", {}, - {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, + {idserver: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}}, ); } @@ -272,7 +272,7 @@ export default class SetIdServer extends React.Component { if (SdkConfig.get()['validated_server_config']['isUrl']) { // Prepopulate the client's default so the user at least has some idea of // a valid value they might enter - newFieldVal = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); + newFieldVal = abbreviateIdentityUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.setState({ @@ -290,12 +290,12 @@ export default class SetIdServer extends React.Component { let sectionTitle; let bodyText; if (idServerUrl) { - sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) }); + sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateIdentityUrl(idServerUrl) }); bodyText = _t( "You are currently using to discover and be discoverable by " + "existing contacts you know. You can change your identity server below.", {}, - { server: sub => {abbreviateUrl(idServerUrl)} }, + { server: sub => {abbreviateIdentityUrl(idServerUrl)} }, ); } else { sectionTitle = _t("Identity Server"); From 525b4cad0f88a184d392b9adfe202b0d4c4be8ed Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 22:54:23 -0600 Subject: [PATCH 155/413] Support IS token handling without checking terms This is so we can optionally do our own terms handling. --- src/IdentityAuthClient.js | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index d3b4d8a6de..075ae93709 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -65,7 +65,7 @@ export default class IdentityAuthClient { } // Returns a promise that resolves to the access_token string from the IS - async getAccessToken() { + async getAccessToken(check=true) { if (!this.authEnabled) { // The current IS doesn't support authentication return null; @@ -77,7 +77,7 @@ export default class IdentityAuthClient { } if (!token) { - token = await this.registerForToken(); + token = await this.registerForToken(check); if (token) { this.accessToken = token; this._writeToken(); @@ -85,18 +85,20 @@ export default class IdentityAuthClient { return token; } - try { - await this._checkToken(token); - } catch (e) { - if (e instanceof TermsNotSignedError) { - // Retrying won't help this - throw e; - } - // Retry in case token expired - token = await this.registerForToken(); - if (token) { - this.accessToken = token; - this._writeToken(); + if (check) { + try { + await this._checkToken(token); + } catch (e) { + if (e instanceof TermsNotSignedError) { + // Retrying won't help this + throw e; + } + // Retry in case token expired + token = await this.registerForToken(); + if (token) { + this.accessToken = token; + this._writeToken(); + } } } @@ -126,12 +128,12 @@ export default class IdentityAuthClient { // See also https://github.com/vector-im/riot-web/issues/10455. } - async registerForToken() { + async registerForToken(check=true) { try { const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); const { access_token: identityAccessToken } = await this._matrixClient.registerWithIdentityServer(hsOpenIdToken); - await this._checkToken(identityAccessToken); + if (check) await this._checkToken(identityAccessToken); return identityAccessToken; } catch (e) { if (e.cors === "rejected" || e.httpStatus === 404) { From 417de0cac7adbcd0aaadc6a563a3199131950d2b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 22:59:33 -0600 Subject: [PATCH 156/413] Add an inline terms agreement component Handles agreement of terms in an inline way. --- res/css/_components.scss | 1 + .../views/terms/_InlineTermsAgreement.scss | 45 +++++++ .../views/terms/InlineTermsAgreement.js | 119 ++++++++++++++++++ src/i18n/strings/en_EN.json | 2 + 4 files changed, 167 insertions(+) create mode 100644 res/css/views/terms/_InlineTermsAgreement.scss create mode 100644 src/components/views/terms/InlineTermsAgreement.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 579369a509..b8811c742f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -180,6 +180,7 @@ @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; +@import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_IncomingCallbox.scss"; diff --git a/res/css/views/terms/_InlineTermsAgreement.scss b/res/css/views/terms/_InlineTermsAgreement.scss new file mode 100644 index 0000000000..e00dcf31d1 --- /dev/null +++ b/res/css/views/terms/_InlineTermsAgreement.scss @@ -0,0 +1,45 @@ +/* +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. +*/ + +.mx_InlineTermsAgreement_cbContainer { + margin-bottom: 10px; + font-size: 14px; + + a { + color: $accent-color; + text-decoration: none; + } + + .mx_InlineTermsAgreement_checkbox { + margin-top: 10px; + + input { + vertical-align: text-bottom; + } + } +} + +.mx_InlineTermsAgreement_link { + display: inline-block; + mask-image: url('$(res)/img/external-link.svg'); + background-color: $accent-color; + mask-repeat: no-repeat; + mask-size: contain; + width: 12px; + height: 12px; + margin-left: 3px; + vertical-align: middle; +} diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js new file mode 100644 index 0000000000..c88612dacb --- /dev/null +++ b/src/components/views/terms/InlineTermsAgreement.js @@ -0,0 +1,119 @@ +/* +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. +*/ + +import React from "react"; +import PropTypes from "prop-types"; +import {_t, pickBestLanguage} from "../../../languageHandler"; +import sdk from "../../../.."; + +export default class InlineTermsAgreement extends React.Component { + static propTypes = { + policiesAndServicePairs: PropTypes.array.isRequired, // array of service/policy pairs + agreedUrls: PropTypes.array.isRequired, // array of URLs the user has accepted + onFinished: PropTypes.func.isRequired, // takes an argument of accepted URLs + introElement: PropTypes.node, + }; + + constructor() { + super(); + + this.state = { + policies: [], + busy: false, + }; + } + + componentDidMount() { + // Build all the terms the user needs to accept + const policies = []; // { checked, url, name } + for (const servicePolicies of this.props.policiesAndServicePairs) { + const availablePolicies = Object.values(servicePolicies.policies); + for (const policy of availablePolicies) { + const language = pickBestLanguage(Object.keys(policy).filter(p => p !== 'version')); + const renderablePolicy = { + checked: false, + url: policy[language].url, + name: policy[language].name, + }; + policies.push(renderablePolicy); + } + } + + this.setState({policies}); + } + + _togglePolicy = (index) => { + const policies = JSON.parse(JSON.stringify(this.state.policies)); // deep & cheap clone + policies[index].checked = !policies[index].checked; + this.setState({policies}); + }; + + _onContinue = () => { + const hasUnchecked = !!this.state.policies.some(p => !p.checked); + if (hasUnchecked) return; + + this.setState({busy: true}); + this.props.onFinished(this.state.policies.map(p => p.url)); + }; + + _renderCheckboxes() { + const rendered = []; + for (let i = 0; i < this.state.policies.length; i++) { + const policy = this.state.policies[i]; + const introText = _t( + "Accept to continue:", {}, { + policyLink: () => { + return ( + + {policy.name} + + + ); + }, + }, + ); + rendered.push( +
+
{introText}
+
+ this._togglePolicy(i)} checked={policy.checked} /> + {_t("Accept")} +
+
+ ); + } + return rendered; + } + + render() { + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + const hasUnchecked = !!this.state.policies.some(p => !p.checked); + + return ( +
+ {this.props.introElement} + {this._renderCheckboxes()} + + {_t("Continue")} + +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d38965bea4..469a31bb43 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -457,6 +457,7 @@ "Headphones": "Headphones", "Folder": "Folder", "Pin": "Pin", + "Accept to continue:": "Accept to continue:", "Failed to upload profile picture!": "Failed to upload profile picture!", "Upload new:": "Upload new:", "No display name": "No display name", @@ -582,6 +583,7 @@ "Set a new account password...": "Set a new account password...", "Language and region": "Language and region", "Theme": "Theme", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", From 318182953298baa12893db462f6be7426cd02961 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 23:00:05 -0600 Subject: [PATCH 157/413] Use new InlineTermsAgreement component on IS Discovery section Fixes https://github.com/vector-im/riot-web/issues/10522 --- .../tabs/user/_GeneralUserSettingsTab.scss | 4 + .../tabs/user/GeneralUserSettingsTab.js | 80 ++++++++++++++++++- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 16467165cf..ae55733192 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -28,3 +28,7 @@ limitations under the License. .mx_GeneralUserSettingsTab_languageInput { @mixin mx_Settings_fullWidthField; } + +.mx_GeneralUserSettingsTab_warningIcon { + vertical-align: middle; +} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 7a8d123fcd..2f9752bb86 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -33,6 +33,10 @@ import MatrixClientPeg from "../../../../../MatrixClientPeg"; import sdk from "../../../../.."; import Modal from "../../../../../Modal"; import dis from "../../../../../dispatcher"; +import {Service, startTermsFlow} from "../../../../../Terms"; +import {SERVICE_TYPES} from "matrix-js-sdk"; +import IdentityAuthClient from "../../../../../IdentityAuthClient"; +import {abbreviateIdentityUrl} from "../../SetIdServer"; export default class GeneralUserSettingsTab extends React.Component { static propTypes = { @@ -47,6 +51,14 @@ export default class GeneralUserSettingsTab extends React.Component { theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), serverRequiresIdServer: null, + idServerHasUnsignedTerms: false, + requiredPolicyInfo: { // This object is passed along to a component for handling + hasTerms: false, + // policiesAndServices, // From the startTermsFlow callback + // agreedUrls, // From the startTermsFlow callback + // resolve, // Promise resolve function for startTermsFlow callback + // reject, // Promise reject function for startTermsFlow callback + }, }; this.dispatcherRef = dis.register(this._onAction); @@ -55,6 +67,9 @@ export default class GeneralUserSettingsTab extends React.Component { async componentWillMount() { const serverRequiresIdServer = await MatrixClientPeg.get().doesServerRequireIdServerParam(); this.setState({serverRequiresIdServer}); + + // Check to see if terms need accepting + this._checkTerms(); } componentWillUnmount() { @@ -64,9 +79,49 @@ export default class GeneralUserSettingsTab extends React.Component { _onAction = (payload) => { if (payload.action === 'id_server_changed') { this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); + this._checkTerms(); } }; + async _checkTerms() { + if (!this.state.haveIdServer) { + this.setState({idServerHasUnsignedTerms: false}); + return; + } + + // By starting the terms flow we get the logic for checking which terms the user has signed + // for free. So we might as well use that for our own purposes. + const authClient = new IdentityAuthClient(); + console.log("Getting access token..."); + const idAccessToken = await authClient.getAccessToken(/*check=*/false); + console.log("Got access token: " + idAccessToken); + startTermsFlow([new Service( + SERVICE_TYPES.IS, + MatrixClientPeg.get().getIdentityServerUrl(), + idAccessToken, + )], (policiesAndServices, agreedUrls, extraClassNames) => { + return new Promise((resolve, reject) => { + this.setState({ + idServerName: abbreviateIdentityUrl(MatrixClientPeg.get().getIdentityServerUrl()), + requiredPolicyInfo: { + hasTerms: true, + policiesAndServices, + agreedUrls, + resolve, + reject, + }, + }); + }); + }).then(() => { + // User accepted all terms + this.setState({ + requiredPolicyInfo: { + hasTerms: false, + }, + }); + }); + } + _onLanguageChange = (newLanguage) => { if (this.state.language === newLanguage) return; @@ -198,6 +253,23 @@ export default class GeneralUserSettingsTab extends React.Component { } _renderDiscoverySection() { + if (this.state.requiredPolicyInfo.hasTerms) { + const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement"); + const intro = + {_t( + "Agree to the identity server (%(serverName)s) Terms of Service to " + + "allow yourself to be discoverable by email address or phone number.", + {serverName: this.state.idServerName}, + )} + ; + return ; + } + const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses"); const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers"); const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); @@ -246,6 +318,12 @@ export default class GeneralUserSettingsTab extends React.Component { } render() { + const discoWarning = this.state.requiredPolicyInfo.hasTerms + ? {_t("Warning")} + : null; + return (
{_t("General")}
@@ -253,7 +331,7 @@ export default class GeneralUserSettingsTab extends React.Component { {this._renderAccountSection()} {this._renderLanguageSection()} {this._renderThemeSection()} -
{_t("Discovery")}
+
{discoWarning} {_t("Discovery")}
{this._renderDiscoverySection()} {this._renderIntegrationManagerSection() /* Has its own title */}
{_t("Deactivate account")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 469a31bb43..de10eb6735 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -587,6 +587,7 @@ "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", + "Warning": "Warning", "General": "General", "Discovery": "Discovery", "Deactivate account": "Deactivate account", @@ -1050,7 +1051,6 @@ "Checking for an update...": "Checking for an update...", "No update available.": "No update available.", "Downloading update...": "Downloading update...", - "Warning": "Warning", "Unknown Address": "Unknown Address", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", From 7cd2fb371806c9e3ffa6fd6b58ebb1a8822d4f78 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 23:03:01 -0600 Subject: [PATCH 158/413] Appease the linter It really wants a trailing comma. --- src/components/views/terms/InlineTermsAgreement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js index c88612dacb..a22359933f 100644 --- a/src/components/views/terms/InlineTermsAgreement.js +++ b/src/components/views/terms/InlineTermsAgreement.js @@ -92,7 +92,7 @@ export default class InlineTermsAgreement extends React.Component { this._togglePolicy(i)} checked={policy.checked} /> {_t("Accept")}
-
+
, ); } return rendered; From b42b2825c401fc8b78b25cf10e8a8a85e1154fa3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 10:43:03 +0200 Subject: [PATCH 159/413] explicitly check for modifier keydown events --- src/components/structures/LoggedInView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 0ef9e362be..17c8e91cb9 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -349,7 +349,8 @@ const LoggedInView = React.createClass({ let handled = false; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); - const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey; + const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey || + ev.key === "Alt" || ev.key === "Control" || ev.key === "Meta" || ev.key === "Shift"; switch (ev.keyCode) { case KeyCode.PAGE_UP: From 29085895a7da78661742db44ad839763b00b1f4d Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 20 Aug 2019 15:55:57 +0300 Subject: [PATCH 160/413] Fix regression on widget panel edit button Due to commit https://github.com/matrix-org/matrix-react-sdk/commit/ffa49df8892fa5377312dce28adb721326a9009e the parameters for the call to open a widget in edit mode from the widget panel in a room has broken. The `screen` parameter needs to be prefixed with `type_` as it was before. This corresponds to parameters supplied when creating the URL when opening a widget in edit mode through Scalar screens. Signed-off-by: Jason Robinson --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index e0dd47326d..9e3570a608 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -266,7 +266,7 @@ export default class AppTile extends React.Component { // TODO: Open the right manager for the widget IntegrationManagers.sharedInstance().getPrimaryManager().open( this.props.room, - this.props.type, + 'type_' + this.props.type, this.props.id, ); } From f55a40001c453e2f27565923f0d2e3b724f85ed0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 20 Aug 2019 13:20:07 -0600 Subject: [PATCH 161/413] Touch up settings: alignment, spacing, error states Fixes https://github.com/vector-im/riot-web/issues/10554 Issues fixed: * Fields were not ~30px from the avatar (too much right margin) * Tooltips overflowed the dialog on some resolutions * SetIdServer didn't have an error state for making the field red * Spacing between sections in Discovery was wrong (fixed by just removing the problematic n+2 selector - it didn't help anything) --- res/css/_common.scss | 7 ++++++- res/css/views/settings/_ProfileSettings.scss | 4 ---- res/css/views/settings/_SetIdServer.scss | 4 ++++ res/css/views/settings/_SetIntegrationManager.scss | 4 ++++ res/css/views/settings/tabs/_SettingsTab.scss | 11 ----------- src/components/views/elements/Field.js | 6 +++++- src/components/views/settings/SetIdServer.js | 5 ++++- .../views/settings/SetIntegrationManager.js | 1 + 8 files changed, 24 insertions(+), 18 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 1b7c8ec938..70ff68e2eb 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -561,5 +561,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } @define-mixin mx_Settings_fullWidthField { - margin-right: 200px; + margin-right: 100px; } + +@define-mixin mx_Settings_tooltip { + // So it fits in the space provided by the page + max-width: 120px; +} \ No newline at end of file diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index afac75986f..3e97a0ff6d 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -26,10 +26,6 @@ limitations under the License. height: 4em; } -.mx_ProfileSettings_controls .mx_Field { - margin-right: 100px; -} - .mx_ProfileSettings_controls .mx_Field:first-child { margin-top: 0; } diff --git a/res/css/views/settings/_SetIdServer.scss b/res/css/views/settings/_SetIdServer.scss index 55ad6eef02..98c64b7218 100644 --- a/res/css/views/settings/_SetIdServer.scss +++ b/res/css/views/settings/_SetIdServer.scss @@ -17,3 +17,7 @@ limitations under the License. .mx_SetIdServer .mx_Field_input { @mixin mx_Settings_fullWidthField; } + +.mx_SetIdServer_tooltip { + @mixin mx_Settings_tooltip; +} diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss index d532492ea8..99537f9eb4 100644 --- a/res/css/views/settings/_SetIntegrationManager.scss +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -31,3 +31,7 @@ limitations under the License. display: inline-block; padding-left: 5px; } + +.mx_SetIntegrationManager_tooltip { + @mixin mx_Settings_tooltip; +} diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 84ff91d341..794c8106be 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -70,17 +70,6 @@ limitations under the License. word-break: break-all; } -.mx_SettingsTab .mx_SettingsTab_subheading:nth-child(n + 2) { - // TODO: This `nth-child(n + 2)` isn't working very well since many sections - // add intermediate elements (mostly because our version of React requires - // them) which throws off the numbering and results in many subheading - // missing margins. - // See also https://github.com/vector-im/riot-web/issues/10554 - // These views have a lot of the same repetitive information on it, so - // give them more visual distinction between the sections. - margin-top: 25px; -} - .mx_SettingsTab a { color: $accent-color-alt; } diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 2d9ef27edd..d79b230569 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -51,6 +51,9 @@ export default class Field extends React.PureComponent { // If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed. tooltipContent: PropTypes.node, + // If specified alongside tooltipContent, the class name to apply to the + // tooltip itself. + tooltipClassName: PropTypes.string, // All other props pass through to the . }; @@ -177,8 +180,9 @@ export default class Field extends React.PureComponent { const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; if (tooltipContent || this.state.feedback) { + const addlClassName = this.props.tooltipClassName ? this.props.tooltipClassName : ''; fieldTooltip = ; diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index f149ca33cc..67387a7c8e 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -319,7 +319,7 @@ export default class SetIdServer extends React.Component { "won't be discoverable by other users and you won't be " + "able to invite others by email or phone.", )} - + {discoButtonContent}

; @@ -341,6 +341,9 @@ export default class SetIdServer extends React.Component { value={this.state.idServer} onChange={this._onIdentityServerChanged} tooltipContent={this._getTooltip()} + tooltipClassName="mx_SetIdServer_tooltip" + disabled={this.state.busy} + flagInvalid={!!this.state.error} /> From 0b2331440bc19be7f650c6ca0f815d1a6dbdf85a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 20 Aug 2019 13:24:51 -0600 Subject: [PATCH 162/413] Newlines mean everything --- res/css/_common.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 70ff68e2eb..859c0006a1 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -567,4 +567,4 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { @define-mixin mx_Settings_tooltip { // So it fits in the space provided by the page max-width: 120px; -} \ No newline at end of file +} From 8c73056693caec6f9e97705e3dbc6205332b50b9 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 21 Aug 2019 16:06:32 +0300 Subject: [PATCH 163/413] Tweak rageshake logging messages Signed-off-by: Jason Robinson --- src/rageshake/rageshake.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index 6366392c04..87c98d105a 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -74,13 +74,17 @@ class ConsoleLogger { // Convert objects and errors to helpful things args = args.map((arg) => { + let msg = ''; if (arg instanceof Error) { - return arg.message + (arg.stack ? `\n${arg.stack}` : ''); + msg = arg.message + (arg.stack ? `\n${arg.stack}` : ''); } else if (typeof(arg) === 'object') { - return JSON.stringify(arg); + msg = JSON.stringify(arg); } else { - return arg; + msg = arg; } + // Do some cleanup + msg = msg.replace(/token=[a-zA-Z0-9-]+/gm, 'token=xxxxx'); + return msg; }); // Some browsers support string formatting which we're not doing here From c758b5d3f186dd244441373b081c037a9a0e6654 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Aug 2019 08:43:42 -0600 Subject: [PATCH 164/413] We don't use reject --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 2f9752bb86..18c18f61d8 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -57,7 +57,6 @@ export default class GeneralUserSettingsTab extends React.Component { // policiesAndServices, // From the startTermsFlow callback // agreedUrls, // From the startTermsFlow callback // resolve, // Promise resolve function for startTermsFlow callback - // reject, // Promise reject function for startTermsFlow callback }, }; @@ -108,7 +107,6 @@ export default class GeneralUserSettingsTab extends React.Component { policiesAndServices, agreedUrls, resolve, - reject, }, }); }); From 2dc28a608f2a3a4b48547bae6fb6de387902893e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Aug 2019 08:46:10 -0600 Subject: [PATCH 165/413] Move URL abbreviation to its own util file --- src/components/views/settings/SetIdServer.js | 49 ++++--------------- .../tabs/user/GeneralUserSettingsTab.js | 4 +- src/utils/UrlUtils.js | 49 +++++++++++++++++++ 3 files changed, 60 insertions(+), 42 deletions(-) create mode 100644 src/utils/UrlUtils.js diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 135d3c32fd..9c2a59bc82 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -25,38 +25,7 @@ import dis from "../../../dispatcher"; import { getThreepidBindStatus } from '../../../boundThreepids'; import IdentityAuthClient from "../../../IdentityAuthClient"; import {SERVICE_TYPES} from "matrix-js-sdk"; - -/** - * If a url has no path component, etc. abbreviate it to just the hostname - * - * @param {string} u The url to be abbreviated - * @returns {string} The abbreviated url - */ -export function abbreviateIdentityUrl(u) { - if (!u) return ''; - - const parsedUrl = url.parse(u); - // if it's something we can't parse as a url then just return it - if (!parsedUrl) return u; - - if (parsedUrl.path === '/') { - // we ignore query / hash parts: these aren't relevant for IS server URLs - return parsedUrl.host; - } - - return u; -} - -function unabbreviateUrl(u) { - if (!u) return ''; - - let longUrl = u; - if (!u.startsWith('https://')) longUrl = 'https://' + u; - const parsed = url.parse(longUrl); - if (parsed.hostname === null) return u; - - return longUrl; -} +import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; /** * Check an IS URL is valid, including liveness check @@ -93,7 +62,7 @@ export default class SetIdServer extends React.Component { if (!MatrixClientPeg.get().getIdentityServerUrl() && SdkConfig.get()['validated_server_config']['isUrl']) { // If no ID server is configured but there's one in the config, prepopulate // the field to help the user. - defaultIdServer = abbreviateIdentityUrl(SdkConfig.get()['validated_server_config']['isUrl']); + defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.state = { @@ -124,7 +93,7 @@ export default class SetIdServer extends React.Component { const fullUrl = MatrixClientPeg.get().getIdentityServerUrl(); let abbr = ''; - if (fullUrl) abbr = abbreviateIdentityUrl(fullUrl); + if (fullUrl) abbr = abbreviateUrl(fullUrl); this.setState({ currentClientIdServer: fullUrl, @@ -234,15 +203,15 @@ export default class SetIdServer extends React.Component { "server . You will need to reconnect to to stop " + "sharing them.", {}, { - idserver: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}, + idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, // XXX: https://github.com/vector-im/riot-web/issues/9086 - idserver2: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}, + idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, }, ); } else { message = _t( "Disconnect from the identity server ?", {}, - {idserver: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}}, + {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, ); } @@ -272,7 +241,7 @@ export default class SetIdServer extends React.Component { if (SdkConfig.get()['validated_server_config']['isUrl']) { // Prepopulate the client's default so the user at least has some idea of // a valid value they might enter - newFieldVal = abbreviateIdentityUrl(SdkConfig.get()['validated_server_config']['isUrl']); + newFieldVal = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.setState({ @@ -290,12 +259,12 @@ export default class SetIdServer extends React.Component { let sectionTitle; let bodyText; if (idServerUrl) { - sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateIdentityUrl(idServerUrl) }); + sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) }); bodyText = _t( "You are currently using to discover and be discoverable by " + "existing contacts you know. You can change your identity server below.", {}, - { server: sub => {abbreviateIdentityUrl(idServerUrl)} }, + { server: sub => {abbreviateUrl(idServerUrl)} }, ); } else { sectionTitle = _t("Identity Server"); diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 18c18f61d8..e37fa003f7 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -36,7 +36,7 @@ import dis from "../../../../../dispatcher"; import {Service, startTermsFlow} from "../../../../../Terms"; import {SERVICE_TYPES} from "matrix-js-sdk"; import IdentityAuthClient from "../../../../../IdentityAuthClient"; -import {abbreviateIdentityUrl} from "../../SetIdServer"; +import {abbreviateUrl} from "../../../../../utils/UrlUtils"; export default class GeneralUserSettingsTab extends React.Component { static propTypes = { @@ -101,7 +101,7 @@ export default class GeneralUserSettingsTab extends React.Component { )], (policiesAndServices, agreedUrls, extraClassNames) => { return new Promise((resolve, reject) => { this.setState({ - idServerName: abbreviateIdentityUrl(MatrixClientPeg.get().getIdentityServerUrl()), + idServerName: abbreviateUrl(MatrixClientPeg.get().getIdentityServerUrl()), requiredPolicyInfo: { hasTerms: true, policiesAndServices, diff --git a/src/utils/UrlUtils.js b/src/utils/UrlUtils.js new file mode 100644 index 0000000000..7b207c128e --- /dev/null +++ b/src/utils/UrlUtils.js @@ -0,0 +1,49 @@ +/* +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. +*/ + +import url from "url"; + +/** + * If a url has no path component, etc. abbreviate it to just the hostname + * + * @param {string} u The url to be abbreviated + * @returns {string} The abbreviated url + */ +export function abbreviateUrl(u) { + if (!u) return ''; + + const parsedUrl = url.parse(u); + // if it's something we can't parse as a url then just return it + if (!parsedUrl) return u; + + if (parsedUrl.path === '/') { + // we ignore query / hash parts: these aren't relevant for IS server URLs + return parsedUrl.host; + } + + return u; +} + +export function unabbreviateUrl(u) { + if (!u) return ''; + + let longUrl = u; + if (!u.startsWith('https://')) longUrl = 'https://' + u; + const parsed = url.parse(longUrl); + if (parsed.hostname === null) return u; + + return longUrl; +} From 5bf0587cc51cb94833d6b39651fd543b739b1c8c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Aug 2019 08:55:19 -0600 Subject: [PATCH 166/413] Don't double translate labs settings SettingsStore.getDisplayName() already calls _t() for us. Fixes https://github.com/vector-im/riot-web/issues/10586 --- src/components/views/settings/tabs/user/LabsUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 23b5e516cd..9c2d49a8bf 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -32,7 +32,7 @@ export class LabsSettingToggle extends React.Component { }; render() { - const label = _t(SettingsStore.getDisplayName(this.props.featureId)); + const label = SettingsStore.getDisplayName(this.props.featureId); const value = SettingsStore.isFeatureEnabled(this.props.featureId); return ; } From 6f7bbf958e8449c5a751ee90f5db360d78e73562 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Aug 2019 12:51:16 -0600 Subject: [PATCH 167/413] Remove tooltipClassName from the Field's input React doesn't want the property on the , so we'll take it off. --- src/components/views/elements/Field.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index d79b230569..08a578b963 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -144,7 +144,7 @@ export default class Field extends React.PureComponent { render() { const { element, prefix, onValidate, children, tooltipContent, flagInvalid, - ...inputProps} = this.props; + tooltipClassName, ...inputProps} = this.props; const inputElement = element || "input"; @@ -180,7 +180,7 @@ export default class Field extends React.PureComponent { const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; if (tooltipContent || this.state.feedback) { - const addlClassName = this.props.tooltipClassName ? this.props.tooltipClassName : ''; + const addlClassName = tooltipClassName ? tooltipClassName : ''; fieldTooltip = Date: Wed, 21 Aug 2019 12:57:54 -0600 Subject: [PATCH 168/413] Remove extraneous logging --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index e37fa003f7..675e271471 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -91,9 +91,7 @@ export default class GeneralUserSettingsTab extends React.Component { // By starting the terms flow we get the logic for checking which terms the user has signed // for free. So we might as well use that for our own purposes. const authClient = new IdentityAuthClient(); - console.log("Getting access token..."); const idAccessToken = await authClient.getAccessToken(/*check=*/false); - console.log("Got access token: " + idAccessToken); startTermsFlow([new Service( SERVICE_TYPES.IS, MatrixClientPeg.get().getIdentityServerUrl(), From 6449016d4b19a1bfe765e3f499cf768a5f7bbfee Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Aug 2019 14:41:25 -0600 Subject: [PATCH 169/413] Fix alignment of discovery section addresses We target the addresses specifically to avoid crushing the subsection text. --- res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss | 2 ++ .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index ae55733192..62d230e752 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -25,6 +25,8 @@ limitations under the License. .mx_GeneralUserSettingsTab_accountSection .mx_EmailAddresses, .mx_GeneralUserSettingsTab_accountSection .mx_PhoneNumbers, +.mx_GeneralUserSettingsTab_discovery .mx_ExistingEmailAddress, +.mx_GeneralUserSettingsTab_discovery .mx_ExistingPhoneNumber, .mx_GeneralUserSettingsTab_languageInput { @mixin mx_Settings_fullWidthField; } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index e37fa003f7..b7e38ff991 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -272,7 +272,7 @@ export default class GeneralUserSettingsTab extends React.Component { const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers"); const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); - const threepidSection = this.state.haveIdServer ?
+ const threepidSection = this.state.haveIdServer ?
{_t("Email addresses")} From 19b7d18e7a7f9226d8ae0b0c2222fe390ba3ce52 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Aug 2019 18:43:29 -0600 Subject: [PATCH 170/413] No-op removals of widgets that don't exist An example of this is setting your very first widget: there's nothing to remove, so you end up with "cannot call .getContent() of undefined" instead. --- src/utils/WidgetUtils.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 12c1578474..06b4eed55b 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -354,7 +354,9 @@ export default class WidgetUtils { if (!client) { throw new Error('User not logged in'); } - const userWidgets = client.getAccountData('m.widgets').getContent() || {}; + const widgets = client.getAccountData('m.widgets'); + if (!widgets) return; + const userWidgets = widgets.getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { if (widget.content && widget.content.type === "m.integration_manager") { delete userWidgets[key]; @@ -382,7 +384,9 @@ export default class WidgetUtils { if (!client) { throw new Error('User not logged in'); } - const userWidgets = client.getAccountData('m.widgets').getContent() || {}; + const widgets = client.getAccountData('m.widgets'); + if (!widgets) return; + const userWidgets = widgets.getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { if (widget.content && widget.content.type === 'm.stickerpicker') { delete userWidgets[key]; From 341fdcd76155fa552bdf6764cd147e0b6725665c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 5 Aug 2019 15:50:16 +0200 Subject: [PATCH 171/413] cleanup lint errors --- src/components/views/rooms/MessageComposer.js | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index adfc0a7999..a14bac5a2a 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -19,7 +19,6 @@ import PropTypes from 'prop-types'; import { _t, _td } from '../../../languageHandler'; import CallHandler from '../../../CallHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import Modal from '../../../Modal'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import RoomViewStore from '../../../stores/RoomViewStore'; @@ -28,7 +27,6 @@ import Stickerpicker from './Stickerpicker'; import { makeRoomPermalink } from '../../../matrix-to'; import ContentMessages from '../../../ContentMessages'; import classNames from 'classnames'; - import E2EIcon from './E2EIcon'; const formatButtonList = [ @@ -51,7 +49,7 @@ function ComposerAvatar(props) { ComposerAvatar.propTypes = { me: PropTypes.object.isRequired, -} +}; function CallButton(props) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -63,15 +61,15 @@ function CallButton(props) { }); }; - return + return (); } CallButton.propTypes = { - roomId: PropTypes.string.isRequired -} + roomId: PropTypes.string.isRequired, +}; function VideoCallButton(props) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -107,15 +105,15 @@ function HangupButton(props) { room_id: call.roomId, }); }; - return ; + return (); } HangupButton.propTypes = { roomId: PropTypes.string.isRequired, -} +}; function FormattingButton(props) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -133,7 +131,7 @@ function FormattingButton(props) { FormattingButton.propTypes = { showFormatting: PropTypes.bool.isRequired, onClickHandler: PropTypes.func.isRequired, -} +}; class UploadButton extends React.Component { static propTypes = { @@ -376,7 +374,7 @@ export default class MessageComposer extends React.Component { height="17" /> ); - }) + }); return (
@@ -401,7 +399,9 @@ export default class MessageComposer extends React.Component { render() { const controls = [ this.state.me ? : null, - this.props.e2eStatus ? : null, + this.props.e2eStatus ? + : + null, ]; if (!this.state.tombstone && this.state.canSendMessages) { @@ -421,8 +421,11 @@ export default class MessageComposer extends React.Component { placeholder={this.renderPlaceholderText()} onInputStateChanged={this.onInputStateChanged} permalinkCreator={this.props.permalinkCreator} />, - showFormattingButton ? : null, + showFormattingButton ? : + null, , , callInProgress ? : null, @@ -485,5 +488,5 @@ MessageComposer.propTypes = { callState: PropTypes.string, // string representing the current room app drawer state - showApps: PropTypes.bool + showApps: PropTypes.bool, }; From 505846ce5345d0cd53bdb5a675a2cbe9efa054e5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 16:31:21 +0200 Subject: [PATCH 172/413] split up css, update class names --- res/css/_components.scss | 3 +- .../views/rooms/_BasicMessageComposer.scss | 55 +++++++++++++++++++ .../_EditMessageComposer.scss} | 46 +++------------- .../views/rooms/BasicMessageComposer.js | 26 ++++----- .../views/rooms/EditMessageComposer.js | 24 ++++---- 5 files changed, 89 insertions(+), 65 deletions(-) create mode 100644 res/css/views/rooms/_BasicMessageComposer.scss rename res/css/views/{elements/_MessageEditor.scss => rooms/_EditMessageComposer.scss} (57%) diff --git a/res/css/_components.scss b/res/css/_components.scss index b8811c742f..34d6f8a900 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -92,7 +92,6 @@ @import "./views/elements/_InteractiveTooltip.scss"; @import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_MemberEventListSummary.scss"; -@import "./views/elements/_MessageEditor.scss"; @import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_ProgressBar.scss"; @import "./views/elements/_ReplyThread.scss"; @@ -135,7 +134,9 @@ @import "./views/rooms/_AppsDrawer.scss"; @import "./views/rooms/_Autocomplete.scss"; @import "./views/rooms/_AuxPanel.scss"; +@import "./views/rooms/_BasicMessageComposer.scss"; @import "./views/rooms/_E2EIcon.scss"; +@import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss new file mode 100644 index 0000000000..cfb957a8c5 --- /dev/null +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -0,0 +1,55 @@ +/* +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. +*/ + +.mx_BasicMessageComposer { + .mx_BasicMessageComposer_input { + padding: 3px 6px; + white-space: pre-wrap; + word-wrap: break-word; + outline: none; + overflow-x: auto; + + span.mx_UserPill, span.mx_RoomPill { + padding-left: 21px; + position: relative; + + // 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; + } + } + } + + .mx_BasicMessageComposer_AutoCompleteWrapper { + position: relative; + height: 0; + } +} diff --git a/res/css/views/elements/_MessageEditor.scss b/res/css/views/rooms/_EditMessageComposer.scss similarity index 57% rename from res/css/views/elements/_MessageEditor.scss rename to res/css/views/rooms/_EditMessageComposer.scss index 7fd99bae17..cfb281b1a0 100644 --- a/res/css/views/elements/_MessageEditor.scss +++ b/res/css/views/rooms/_EditMessageComposer.scss @@ -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. @@ -14,8 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MessageEditor { - border-radius: 4px; +.mx_EditMessageComposer { + padding: 3px; // this is to try not make the text move but still have some // padding around and in the editor. @@ -23,47 +24,19 @@ limitations under the License. margin: -7px -10px -5px -10px; overflow: visible !important; // override mx_EventTile_content - .mx_MessageEditor_editor { + + .mx_BasicMessageComposer_input { border-radius: 4px; border: solid 1px $primary-hairline-color; background-color: $primary-bg-color; - padding: 3px 6px; - white-space: pre-wrap; - word-wrap: break-word; - outline: none; max-height: 200px; - overflow-x: auto; &:focus { border-color: $accent-color-50pct; } - - span.mx_UserPill, span.mx_RoomPill { - padding-left: 21px; - position: relative; - - // 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; - } - } } - .mx_MessageEditor_buttons { + .mx_EditMessageComposer_buttons { display: flex; flex-direction: row; justify-content: flex-end; @@ -81,14 +54,9 @@ limitations under the License. padding: 5px 40px; } } - - .mx_MessageEditor_AutoCompleteWrapper { - position: relative; - height: 0; - } } -.mx_EventTile_last .mx_MessageEditor_buttons { +.mx_EventTile_last .mx_EditMessageComposer_buttons { position: static; margin-right: -147px; } diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 76de9a6794..a5283cc15d 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -206,7 +206,7 @@ export default class BasicMessageEditor extends React.Component { if (this.state.autoComplete) { const query = this.state.query; const queryLen = query.length; - autoComplete =
+ autoComplete = (
this._autocompleteRef = ref} query={query} @@ -215,18 +215,18 @@ export default class BasicMessageEditor extends React.Component { selection={{beginning: true, end: queryLen, start: queryLen}} room={this.props.room} /> -
; +
); } - return
- { autoComplete } -
this._editorRef = ref} - aria-label={_t("Edit message")} - >
-
; + return (
+ { autoComplete } +
this._editorRef = ref} + aria-label={_t("Edit message")} + >
+
); } } diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 3ba14d9369..87e4a4a665 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -249,17 +249,17 @@ export default class EditMessageComposer extends React.Component { render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return
- -
- {_t("Cancel")} - {_t("Save")} -
-
; + return (
+ +
+ {_t("Cancel")} + {_t("Save")} +
+
); } } From 063eabed7108c1fa8d7e69b3553b4da79647d5e8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 16:32:01 +0200 Subject: [PATCH 173/413] don't return invalid indices from model, fix for #10358 --- src/editor/model.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/editor/model.js b/src/editor/model.js index 74546b9bf8..759e13aabb 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -80,7 +80,8 @@ export default class EditorModel { const part = this._parts[index]; return new DocumentPosition(index, part.text.length); } else { - return new DocumentPosition(0, 0); + // part index -1, as there are no parts to point at + return new DocumentPosition(-1, 0); } } From d22745a5b293fa1244e60efdcfce9fd58e3bcd34 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 16:32:43 +0200 Subject: [PATCH 174/413] make it obvious arguments are optional because now they have a setter --- src/editor/model.js | 2 +- src/editor/parts.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index 759e13aabb..580085975f 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -18,7 +18,7 @@ limitations under the License. import {diffAtCaret, diffDeletion} from "./diff"; export default class EditorModel { - constructor(parts, partCreator, updateCallback) { + constructor(parts, partCreator, updateCallback = null) { this._parts = parts; this._partCreator = partCreator; this._activePartIdx = null; diff --git a/src/editor/parts.js b/src/editor/parts.js index 2a6ad81b9b..d3569a7347 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -363,7 +363,7 @@ export function autoCompleteCreator(getAutocompleterComponent, updateQuery) { } export class PartCreator { - constructor(room, client, autoCompleteCreator) { + constructor(room, client, autoCompleteCreator = null) { this._room = room; this._client = client; this._autoCompleteCreator = {create: autoCompleteCreator && autoCompleteCreator(this)}; From 92d72630469f4345cd8e5a0f92e1e51244e6dc91 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 17:02:47 +0200 Subject: [PATCH 175/413] move editor padding to edit specific style file as it will be different for the main composer --- res/css/views/rooms/_BasicMessageComposer.scss | 1 - res/css/views/rooms/_EditMessageComposer.scss | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index cfb957a8c5..a4f2afd795 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -17,7 +17,6 @@ limitations under the License. .mx_BasicMessageComposer { .mx_BasicMessageComposer_input { - padding: 3px 6px; white-space: pre-wrap; word-wrap: break-word; outline: none; diff --git a/res/css/views/rooms/_EditMessageComposer.scss b/res/css/views/rooms/_EditMessageComposer.scss index cfb281b1a0..214bfc4a1a 100644 --- a/res/css/views/rooms/_EditMessageComposer.scss +++ b/res/css/views/rooms/_EditMessageComposer.scss @@ -30,6 +30,7 @@ limitations under the License. border: solid 1px $primary-hairline-color; background-color: $primary-bg-color; max-height: 200px; + padding: 3px 6px; &:focus { border-color: $accent-color-50pct; From df8488e194cc83b1b366cb16943c16975ed69f8a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 17:03:16 +0200 Subject: [PATCH 176/413] pass label through props --- src/components/views/rooms/BasicMessageComposer.js | 2 +- src/components/views/rooms/EditMessageComposer.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index a5283cc15d..e498ff88ad 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -225,7 +225,7 @@ export default class BasicMessageEditor extends React.Component { tabIndex="1" onKeyDown={this._onKeyDown} ref={ref => this._editorRef = ref} - aria-label={_t("Edit message")} + aria-label={this.props.label} >
); } diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 87e4a4a665..7330405b81 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -255,6 +255,7 @@ export default class EditMessageComposer extends React.Component { model={this.model} room={this._getRoom()} initialCaret={this.props.editState.getCaret()} + label={_t("Edit message")} />
{_t("Cancel")} From cfbd2e9cc8863d6b64633edbb12415b75dea1845 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 17:03:44 +0200 Subject: [PATCH 177/413] support basic sending with new main composer this removes all formatting options, as the new editor doesn't have any. --- res/css/_components.scss | 1 + res/css/views/rooms/_SendMessageComposer.scss | 30 +++++ src/components/views/rooms/MessageComposer.js | 121 +----------------- .../views/rooms/SendMessageComposer.js | 105 +++++++++++++++ 4 files changed, 141 insertions(+), 116 deletions(-) create mode 100644 res/css/views/rooms/_SendMessageComposer.scss create mode 100644 src/components/views/rooms/SendMessageComposer.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 34d6f8a900..d19d07132c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -159,6 +159,7 @@ @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_SearchBar.scss"; @import "./views/rooms/_SearchableEntityList.scss"; +@import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss"; @import "./views/rooms/_WhoIsTypingTile.scss"; diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss new file mode 100644 index 0000000000..7bb20a443b --- /dev/null +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -0,0 +1,30 @@ +/* +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. +*/ + +.mx_SendMessageComposer { + flex: 1; + min-height: 50px; + display: flex; + flex-direction: column; + justify-content: center; + + .mx_BasicMessageComposer { + .mx_BasicMessageComposer_input { + padding: 3px 0; + } + } +} + diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index a14bac5a2a..022d45e60e 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -16,30 +16,18 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; -import { _t, _td } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import CallHandler from '../../../CallHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import RoomViewStore from '../../../stores/RoomViewStore'; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import Stickerpicker from './Stickerpicker'; import { makeRoomPermalink } from '../../../matrix-to'; import ContentMessages from '../../../ContentMessages'; import classNames from 'classnames'; import E2EIcon from './E2EIcon'; -const formatButtonList = [ - _td("bold"), - _td("italic"), - _td("deleted"), - _td("underlined"), - _td("inline-code"), - _td("block-quote"), - _td("bulleted-list"), - _td("numbered-list"), -]; - function ComposerAvatar(props) { const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); return
@@ -115,28 +103,11 @@ HangupButton.propTypes = { roomId: PropTypes.string.isRequired, }; -function FormattingButton(props) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return ; -} - -FormattingButton.propTypes = { - showFormatting: PropTypes.bool.isRequired, - onClickHandler: PropTypes.func.isRequired, -}; - class UploadButton extends React.Component { static propTypes = { roomId: PropTypes.string.isRequired, } + constructor(props, context) { super(props, context); this.onUploadClick = this.onUploadClick.bind(this); @@ -193,24 +164,14 @@ class UploadButton extends React.Component { export default class MessageComposer extends React.Component { constructor(props, context) { super(props, context); - this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); - this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this); - this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this); this.onInputStateChanged = this.onInputStateChanged.bind(this); this.onEvent = this.onEvent.bind(this); this._onRoomStateEvents = this._onRoomStateEvents.bind(this); this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); this._onTombstoneClick = this._onTombstoneClick.bind(this); this.renderPlaceholderText = this.renderPlaceholderText.bind(this); - this.renderFormatBar = this.renderFormatBar.bind(this); this.state = { - inputState: { - marks: [], - blockType: null, - isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'), - }, - showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'), isQuoting: Boolean(RoomViewStore.getQuotingEvent()), tombstone: this._getRoomTombstone(), canSendMessages: this.props.room.maySendMessage(), @@ -257,6 +218,7 @@ export default class MessageComposer extends React.Component { onEvent(event) { if (event.getType() !== 'm.room.encryption') return; if (event.getRoomId() !== this.props.room.roomId) return; + // TODO: put (encryption state??) in state this.forceUpdate(); } @@ -281,34 +243,12 @@ export default class MessageComposer extends React.Component { this.setState({ isQuoting }); } - onInputStateChanged(inputState) { // Merge the new input state with old to support partial updates inputState = Object.assign({}, this.state.inputState, inputState); this.setState({inputState}); } - _onAutocompleteConfirm(range, completion) { - if (this.messageComposerInput) { - this.messageComposerInput.setDisplayedCompletion(range, completion); - } - } - - onFormatButtonClicked(name, event) { - event.preventDefault(); - this.messageComposerInput.onFormatButtonClicked(name, event); - } - - onToggleFormattingClicked() { - SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting); - this.setState({showFormatting: !this.state.showFormatting}); - } - - onToggleMarkdownClicked(e) { - e.preventDefault(); // don't steal focus from the editor! - this.messageComposerInput.enableRichtext(!this.state.inputState.isRichTextEnabled); - } - _onTombstoneClick(ev) { ev.preventDefault(); @@ -355,47 +295,6 @@ export default class MessageComposer extends React.Component { } } - renderFormatBar() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const {marks, blockType} = this.state.inputState; - const formatButtons = formatButtonList.map((name) => { - // special-case to match the md serializer and the special-case in MessageComposerInput.js - const markName = name === 'inline-code' ? 'code' : name; - const active = marks.some(mark => mark.type === markName) || blockType === name; - const suffix = active ? '-on' : ''; - const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name); - const className = 'mx_MessageComposer_format_button mx_filterFlipColor'; - return ( - - ); - }); - - return ( -
-
- { formatButtons } -
- - -
-
- ); - } - render() { const controls = [ this.state.me ? : null, @@ -409,23 +308,16 @@ export default class MessageComposer extends React.Component { // check separately for whether we can call, but this is slightly // complex because of conference calls. - const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput"); - const showFormattingButton = this.state.inputState.isRichTextEnabled; + const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer"); const callInProgress = this.props.callState && this.props.callState !== 'ended'; controls.push( - this.messageComposerInput = c} key="controls_input" room={this.props.room} placeholder={this.renderPlaceholderText()} - onInputStateChanged={this.onInputStateChanged} permalinkCreator={this.props.permalinkCreator} />, - showFormattingButton ? : - null, , , callInProgress ? : null, @@ -461,8 +353,6 @@ export default class MessageComposer extends React.Component { ); } - const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled; - const wrapperClasses = classNames({ mx_MessageComposer_wrapper: true, mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus, @@ -474,7 +364,6 @@ export default class MessageComposer extends React.Component { { controls }
- { showFormatBar ? this.renderFormatBar() : null }
); } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js new file mode 100644 index 0000000000..50a09eb894 --- /dev/null +++ b/src/components/views/rooms/SendMessageComposer.js @@ -0,0 +1,105 @@ +/* +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. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import dis from '../../../dispatcher'; +import EditorModel from '../../../editor/model'; +import {getCaretOffsetAndText} from '../../../editor/dom'; +import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize'; +import {PartCreator} from '../../../editor/parts'; +import EditorStateTransfer from '../../../utils/EditorStateTransfer'; +import {MatrixClient} from 'matrix-js-sdk'; +import BasicMessageComposer from "./BasicMessageComposer"; + +function createMessageContent(model, editedEvent) { + const body = textSerialize(model); + const content = { + msgtype: "m.text", + body, + }; + const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: false}); + if (formattedBody) { + content.format = "org.matrix.custom.html"; + content.formatted_body = formattedBody; + } + return content; +} + +export default class SendMessageComposer extends React.Component { + static propTypes = { + // the message event being edited + editState: PropTypes.instanceOf(EditorStateTransfer).isRequired, + }; + + static contextTypes = { + matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + }; + + constructor(props, context) { + super(props, context); + this.model = null; + this._editorRef = null; + } + + _setEditorRef = ref => { + this._editorRef = ref; + }; + + _onKeyDown = (event) => { + if (event.metaKey || event.altKey || event.shiftKey) { + return; + } + if (event.key === "Enter") { + this._sendMessage(); + event.preventDefault(); + } + } + + _sendMessage = () => { + const {roomId} = this.props.room; + this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model)); + this.model.reset([]); + dis.dispatch({action: 'focus_composer'}); + } + + componentWillUnmount() { + const sel = document.getSelection(); + const {caret} = getCaretOffsetAndText(this._editorRef, sel); + const parts = this.model.serializeParts(); + this.props.editState.setEditorState(caret, parts); + } + + componentWillMount() { + const partCreator = new PartCreator(this.props.room, this.context.matrixClient); + this.model = new EditorModel([], partCreator); + } + + render() { + //
+ //
+ // + return ( +
+ +
+ ); + } +} From e2e4ea493fa239583681b4aee76a2378fe368ebb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 17:22:26 +0200 Subject: [PATCH 178/413] set aria label on main composer too --- src/components/views/rooms/SendMessageComposer.js | 4 +++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 50a09eb894..7f17ceba6e 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -24,6 +24,7 @@ import {PartCreator} from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; +import { _t } from '../../../languageHandler'; function createMessageContent(model, editedEvent) { const body = textSerialize(model); @@ -69,7 +70,7 @@ export default class SendMessageComposer extends React.Component { } } - _sendMessage = () => { + _sendMessage() { const {roomId} = this.props.room; this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model)); this.model.reset([]); @@ -98,6 +99,7 @@ export default class SendMessageComposer extends React.Component { ref={this._setEditorRef} model={this.model} room={this.props.room} + label={_t("Send message")} />
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fd5e42bcb4..53d8529c65 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -923,6 +923,7 @@ "This Room": "This Room", "All Rooms": "All Rooms", "Search…": "Search…", + "Send message": "Send message", "Failed to connect to integrations server": "Failed to connect to integrations server", "No integrations server is configured to manage stickers with": "No integrations server is configured to manage stickers with", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", From f9992a1fc685cb325d5560f5a2cb9998579bd21b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 17:52:47 +0200 Subject: [PATCH 179/413] implement editor placeholder --- res/css/views/rooms/_BasicMessageComposer.scss | 11 +++++++++++ src/components/views/rooms/BasicMessageComposer.js | 10 ++++++++++ src/components/views/rooms/SendMessageComposer.js | 1 + src/editor/model.js | 4 ++++ 4 files changed, 26 insertions(+) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index a4f2afd795..acec4de952 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -16,6 +16,17 @@ limitations under the License. */ .mx_BasicMessageComposer { + .mx_BasicMessageComposer_inputEmpty > :first-child:before { + content: var(--placeholder); + opacity: 0.333; + width: 0; + height: 0; + overflow: visible; + display: inline-block; + pointer-events: none; + white-space: nowrap; + } + .mx_BasicMessageComposer_input { white-space: pre-wrap; word-wrap: break-word; diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index e498ff88ad..ca8681bf4d 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -54,6 +54,16 @@ export default class BasicMessageEditor extends React.Component { console.error(err); } } + if (this.props.placeholder) { + const {isEmpty} = this.props.model; + if (isEmpty) { + this._editorRef.style.setProperty("--placeholder", `'${this.props.placeholder}'`); + this._editorRef.classList.add("mx_BasicMessageComposer_inputEmpty"); + } else { + this._editorRef.classList.remove("mx_BasicMessageComposer_inputEmpty"); + this._editorRef.style.removeProperty("--placeholder"); + } + } this.setState({autoComplete: this.props.model.autoComplete}); this.historyManager.tryPush(this.props.model, caret, inputType, diff); } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 7f17ceba6e..8cc988f6a1 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -100,6 +100,7 @@ export default class SendMessageComposer extends React.Component { model={this.model} room={this.props.room} label={_t("Send message")} + placeholder={this.props.placeholder} />
); diff --git a/src/editor/model.js b/src/editor/model.js index 580085975f..64986cdaf2 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -35,6 +35,10 @@ export default class EditorModel { return this._partCreator; } + get isEmpty() { + return this._parts.reduce((len, part) => len + part.text.length, 0) === 0; + } + clone() { return new EditorModel(this._parts, this._partCreator, this._updateCallback); } From 64e83a81111cbf909b765604c79559b6756a8857 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 17:53:06 +0200 Subject: [PATCH 180/413] font-size --- res/css/views/rooms/_SendMessageComposer.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index 7bb20a443b..bdc96489d4 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -20,6 +20,7 @@ limitations under the License. display: flex; flex-direction: column; justify-content: center; + font-size: 14px; .mx_BasicMessageComposer { .mx_BasicMessageComposer_input { From 2cff486ec0c0f2aa7de9ef91375ab0adcae4b167 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 6 Aug 2019 17:53:23 +0200 Subject: [PATCH 181/413] set placeholder same as label --- src/components/views/rooms/SendMessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 8cc988f6a1..0bdb901dc9 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -99,7 +99,7 @@ export default class SendMessageComposer extends React.Component { ref={this._setEditorRef} model={this.model} room={this.props.room} - label={_t("Send message")} + label={this.props.placeholder} placeholder={this.props.placeholder} />
From 33c6945fc4f5f9a39ed1527a125e17e0e051cb0b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Aug 2019 14:06:44 +0200 Subject: [PATCH 182/413] align autocomplete at top of composer --- res/css/views/rooms/_SendMessageComposer.scss | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index bdc96489d4..ce5043a5bb 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -19,12 +19,22 @@ limitations under the License. min-height: 50px; display: flex; flex-direction: column; - justify-content: center; font-size: 14px; + justify-content: center; + display: flex; .mx_BasicMessageComposer { + flex: 1; + display: flex; + flex-direction: column; + .mx_BasicMessageComposer_input { padding: 3px 0; + // this will center the contenteditable + // in it's parent vertically + // while keeping the autocomplete at the top + // of the composer. The parent needs to be a flex container for this to work. + margin: auto 0; } } } From fdf5fca6284b06a4ab6ca845ac6047ef374bd244 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Aug 2019 14:07:48 +0200 Subject: [PATCH 183/413] add all props to proptypes --- src/components/views/rooms/BasicMessageComposer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index ca8681bf4d..bf006ca815 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; -import {_t} from '../../../languageHandler'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; @@ -33,6 +32,9 @@ export default class BasicMessageEditor extends React.Component { static propTypes = { model: PropTypes.instanceOf(EditorModel).isRequired, room: PropTypes.instanceOf(Room).isRequired, + placeholder: PropTypes.string, + label: PropTypes.string, // the aria label + initialCaret: PropTypes.object, // See DocumentPosition in editor/model.js }; constructor(props, context) { From d4ca087abe7c866704093d0b9b2f9e51f907413a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Aug 2019 14:51:49 +0200 Subject: [PATCH 184/413] fix styling issues - grow/shrink between min and max height correctly - don't grow wider than available space - some space between editor and buttons --- res/css/views/rooms/_SendMessageComposer.scss | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index ce5043a5bb..ffa20d29fa 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -16,17 +16,22 @@ limitations under the License. .mx_SendMessageComposer { flex: 1; - min-height: 50px; display: flex; flex-direction: column; font-size: 14px; justify-content: center; display: flex; + margin-right: 6px; + // don't grow wider than available space + min-width: 0; .mx_BasicMessageComposer { flex: 1; display: flex; flex-direction: column; + // min-height at this level so the mx_BasicMessageComposer_input + // still stays vertically centered when less than 50px + min-height: 50px; .mx_BasicMessageComposer_input { padding: 3px 0; @@ -35,6 +40,9 @@ limitations under the License. // while keeping the autocomplete at the top // of the composer. The parent needs to be a flex container for this to work. margin: auto 0; + // max-height at this level so autocomplete doesn't get scrolled too + max-height: 140px; + overflow-y: auto; } } } From e39c405c55ed58113218b6d5b2167bacc21c413b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Aug 2019 15:14:16 +0200 Subject: [PATCH 185/413] restore focus_composer action --- src/components/views/rooms/BasicMessageComposer.js | 4 ++++ src/components/views/rooms/SendMessageComposer.js | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index bf006ca815..4a17ec6066 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -241,4 +241,8 @@ export default class BasicMessageEditor extends React.Component { > ); } + + focus() { + this._editorRef.focus(); + } } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 0bdb901dc9..301a0f08d1 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -82,13 +82,23 @@ export default class SendMessageComposer extends React.Component { const {caret} = getCaretOffsetAndText(this._editorRef, sel); const parts = this.model.serializeParts(); this.props.editState.setEditorState(caret, parts); + dis.unregister(this.dispatcherRef); } componentWillMount() { const partCreator = new PartCreator(this.props.room, this.context.matrixClient); this.model = new EditorModel([], partCreator); + this.dispatcherRef = dis.register(this.onAction); } + onAction = (payload) => { + switch (payload.action) { + case 'focus_composer': + this._editorRef.focus(); + break; + } + }; + render() { //
//
From 71286b5610454dfdf8cb1c8282a1a61361d9db3b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Aug 2019 15:14:50 +0200 Subject: [PATCH 186/413] restore reply_to_event action --- res/css/views/rooms/_SendMessageComposer.scss | 5 +++++ src/components/views/rooms/SendMessageComposer.js | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index ffa20d29fa..3304003d84 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -45,5 +45,10 @@ limitations under the License. overflow-y: auto; } } + + .mx_SendMessageComposer_overlayWrapper { + position: relative; + height: 0; + } } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 301a0f08d1..c45ecaa9f3 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -24,7 +24,7 @@ import {PartCreator} from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; -import { _t } from '../../../languageHandler'; +import ReplyPreview from "./ReplyPreview"; function createMessageContent(model, editedEvent) { const body = textSerialize(model); @@ -93,6 +93,7 @@ export default class SendMessageComposer extends React.Component { onAction = (payload) => { switch (payload.action) { + case 'reply_to_event': case 'focus_composer': this._editorRef.focus(); break; @@ -100,11 +101,11 @@ export default class SendMessageComposer extends React.Component { }; render() { - //
- //
- // return (
+
+ +
Date: Wed, 7 Aug 2019 17:44:49 +0200 Subject: [PATCH 187/413] restore insert mention for this, we need to store the last caret in the editor, to know where to insert the user pill. Because clicking on a member blurs the editor, and the selection is moved away from the editor. For this reason, we keep as cache of the last caretOffset object, invalidated by a selection with different values. The selection needs to be cloned because apparently the browser mutates the object instead of returning a new one. --- .../views/rooms/BasicMessageComposer.js | 93 +++++++++++++++---- .../views/rooms/SendMessageComposer.js | 9 ++ src/editor/model.js | 26 ++++++ 3 files changed, 111 insertions(+), 17 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 4a17ec6066..62e136000f 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -28,6 +28,28 @@ import {Room} from 'matrix-js-sdk'; const IS_MAC = navigator.platform.indexOf("Mac") !== -1; +function cloneSelection(selection) { + return { + anchorNode: selection.anchorNode, + anchorOffset: selection.anchorOffset, + focusNode: selection.focusNode, + focusOffset: selection.focusOffset, + isCollapsed: selection.isCollapsed, + rangeCount: selection.rangeCount, + type: selection.type, + }; +} + +function selectionEquals(a: Selection, b: Selection): boolean { + return a.anchorNode === b.anchorNode && + a.anchorOffset === b.anchorOffset && + a.focusNode === b.focusNode && + a.focusOffset === b.focusOffset && + a.isCollapsed === b.isCollapsed && + a.rangeCount === b.rangeCount && + a.type === b.type; +} + export default class BasicMessageEditor extends React.Component { static propTypes = { model: PropTypes.instanceOf(EditorModel).isRequired, @@ -74,6 +96,7 @@ export default class BasicMessageEditor extends React.Component { this._modifiedFlag = true; const sel = document.getSelection(); const {caret, text} = getCaretOffsetAndText(this._editorRef, sel); + this._setLastCaret(caret, text, sel); this.props.model.update(text, event.inputType, caret); } @@ -85,14 +108,59 @@ export default class BasicMessageEditor extends React.Component { this.props.model.update(newText, inputType, caret); } - _isCaretAtStart() { - const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection()); - return caret.offset === 0; + // this is used later to see if we need to recalculate the caret + // on selectionchange. If it is just a consequence of typing + // we don't need to. But if the user is navigating the caret without input + // we need to recalculate it, to be able to know where to insert content after + // losing focus + _setLastCaret(caret, text, selection) { + this._lastSelection = cloneSelection(selection); + this._lastCaret = caret; + this._lastTextLength = text.length; } - _isCaretAtEnd() { - const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection()); - return caret.offset === text.length; + _refreshLastCaretIfNeeded() { + // TODO: needed when going up and down in editing messages ... not sure why yet + // because the editors should stop doing this when when blurred ... + // maybe it's on focus and the _editorRef isn't available yet or something. + if (!this._editorRef) { + return; + } + const selection = document.getSelection(); + if (!this._lastSelection || !selectionEquals(this._lastSelection, selection)) { + this._lastSelection = cloneSelection(selection); + const {caret, text} = getCaretOffsetAndText(this._editorRef, selection); + this._lastCaret = caret; + this._lastTextLength = text.length; + } + return this._lastCaret; + } + + getCaret() { + return this._lastCaret; + } + + isCaretAtStart() { + return this.getCaret().offset === 0; + } + + isCaretAtEnd() { + return this.getCaret().offset === this._lastTextLength; + } + + _onBlur = () => { + document.removeEventListener("selectionchange", this._onSelectionChange); + } + + _onFocus = () => { + document.addEventListener("selectionchange", this._onSelectionChange); + // force to recalculate + this._lastSelection = null; + this._refreshLastCaretIfNeeded(); + } + + _onSelectionChange = () => { + this._refreshLastCaretIfNeeded(); } _onKeyDown = (event) => { @@ -202,17 +270,6 @@ export default class BasicMessageEditor extends React.Component { return caretPosition; } - - isCaretAtStart() { - const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection()); - return caret.offset === 0; - } - - isCaretAtEnd() { - const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection()); - return caret.offset === text.length; - } - render() { let autoComplete; if (this.state.autoComplete) { @@ -235,6 +292,8 @@ export default class BasicMessageEditor extends React.Component { className="mx_BasicMessageComposer_input" contentEditable="true" tabIndex="1" + onBlur={this._onBlur} + onFocus={this._onFocus} onKeyDown={this._onKeyDown} ref={ref => this._editorRef = ref} aria-label={this.props.label} diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index c45ecaa9f3..fdc5fdd9e2 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -97,6 +97,15 @@ export default class SendMessageComposer extends React.Component { case 'focus_composer': this._editorRef.focus(); break; + case 'insert_mention': { + const userId = payload.user_id; + const member = this.props.room.getMember(userId); + const displayName = member ? + member.rawDisplayName : payload.user_id; + const userPillPart = this.model.partCreator.userPill(displayName, userId); + this.model.insertPartAt(userPillPart, this._editorRef.getCaret()); + break; + } } }; diff --git a/src/editor/model.js b/src/editor/model.js index 64986cdaf2..3a8c02da1b 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -108,6 +108,15 @@ export default class EditorModel { this._updateCallback(caret, inputType); } + insertPartAt(part, caret) { + const position = this.positionForOffset(caret.offset, caret.atNodeEnd); + const insertIndex = this._splitAt(position); + this._insertPart(insertIndex, part); + // want to put caret after new part? + const newPosition = new DocumentPosition(insertIndex, part.text.length); + this._updateCallback(newPosition); + } + update(newValue, inputType, caret) { const diff = this._diff(newValue, inputType, caret); const position = this.positionForOffset(diff.at, caret.atNodeEnd); @@ -232,6 +241,23 @@ export default class EditorModel { } return removedOffsetDecrease; } + // return part index where insertion will insert between at offset + _splitAt(pos) { + if (pos.index === -1) { + return 0; + } + if (pos.offset === 0) { + return pos.index; + } + const part = this._parts[pos.index]; + if (pos.offset >= part.text.length) { + return pos.index + 1; + } + + const secondPart = part.split(pos.offset); + this._insertPart(pos.index + 1, secondPart); + return pos.index + 1; + } /** * inserts `str` into the model at `pos`. From 7b3282185ab2a82bd2508a585f91e234d1d593b5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Aug 2019 17:47:24 +0200 Subject: [PATCH 188/413] update proptypes --- src/components/views/rooms/SendMessageComposer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index fdc5fdd9e2..b364ebdcc7 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -21,7 +21,6 @@ import EditorModel from '../../../editor/model'; import {getCaretOffsetAndText} from '../../../editor/dom'; import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize'; import {PartCreator} from '../../../editor/parts'; -import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyPreview from "./ReplyPreview"; @@ -42,8 +41,9 @@ function createMessageContent(model, editedEvent) { export default class SendMessageComposer extends React.Component { static propTypes = { - // the message event being edited - editState: PropTypes.instanceOf(EditorStateTransfer).isRequired, + room: PropTypes.object.isRequired, + placeholder: PropTypes.string, + permalinkCreator: PropTypes.object.isRequired, }; static contextTypes = { From a9d6d01f1036f9855ce9f1a7b7c47feaaccc18c8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 11:41:13 +0200 Subject: [PATCH 189/413] add reply fields to message content to be sent when replying --- .../views/rooms/SendMessageComposer.js | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index b364ebdcc7..916f93d544 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -24,18 +24,49 @@ import {PartCreator} from '../../../editor/parts'; import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyPreview from "./ReplyPreview"; +import RoomViewStore from '../../../stores/RoomViewStore'; +import ReplyThread from "../elements/ReplyThread"; + +function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { + const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); + Object.assign(content, replyContent); + + // Part of Replies fallback support - prepend the text we're sending + // with the text we're replying to + const nestedReply = ReplyThread.getNestedReplyText(repliedToEvent, permalinkCreator); + if (nestedReply) { + if (content.formatted_body) { + content.formatted_body = nestedReply.html + content.formatted_body; + } + content.body = nestedReply.body + content.body; + } + + // Clear reply_to_event as we put the message into the queue + // if the send fails, retry will handle resending. + dis.dispatch({ + action: 'reply_to_event', + event: null, + }); +} + +function createMessageContent(model, permalinkCreator) { + const repliedToEvent = RoomViewStore.getQuotingEvent(); -function createMessageContent(model, editedEvent) { const body = textSerialize(model); const content = { msgtype: "m.text", - body, + body: body, }; - const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: false}); + const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!repliedToEvent}); if (formattedBody) { content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; } + + if (repliedToEvent) { + addReplyToMessageContent(content, repliedToEvent, permalinkCreator); + } + return content; } @@ -72,7 +103,7 @@ export default class SendMessageComposer extends React.Component { _sendMessage() { const {roomId} = this.props.room; - this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model)); + this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model, this.props.permalinkCreator)); this.model.reset([]); dis.dispatch({action: 'focus_composer'}); } From d4fbe7ed691fc98840aed30b5648ef4aa45b01e8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 12:34:35 +0200 Subject: [PATCH 190/413] make editor event parsing suitable for parsing messages to be quoted --- src/editor/deserialize.js | 41 +++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index e8fd8fb888..08827ca89a 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -130,29 +130,29 @@ function checkIgnored(n) { return true; } +const QUOTE_LINE_PREFIX = "> "; function prefixQuoteLines(isFirstNode, parts, partCreator) { - const PREFIX = "> "; // a newline (to append a > to) wouldn't be added to parts for the first line // if there was no content before the BLOCKQUOTE, so handle that if (isFirstNode) { - parts.splice(0, 0, partCreator.plain(PREFIX)); + parts.splice(0, 0, partCreator.plain(QUOTE_LINE_PREFIX)); } for (let i = 0; i < parts.length; i += 1) { if (parts[i].type === "newline") { - parts.splice(i + 1, 0, partCreator.plain(PREFIX)); + parts.splice(i + 1, 0, partCreator.plain(QUOTE_LINE_PREFIX)); i += 1; } } } -function parseHtmlMessage(html, partCreator) { +function parseHtmlMessage(html, partCreator, isQuotedMessage) { // no nodes from parsing here should be inserted in the document, // as scripts in event handlers, etc would be executed then. // we're only taking text, so that is fine const rootNode = new DOMParser().parseFromString(html, "text/html").body; const parts = []; let lastNode; - let inQuote = false; + let inQuote = isQuotedMessage; const state = {}; function onNodeEnter(n) { @@ -220,22 +220,29 @@ function parseHtmlMessage(html, partCreator) { return parts; } -export function parseEvent(event, partCreator) { +function parsePlainTextMessage(body, partCreator, isQuotedMessage) { + const lines = body.split("\n"); + const parts = lines.reduce((parts, line, i) => { + const isLast = i === lines.length - 1; + if (!isLast) { + parts.push(partCreator.newline()); + } + if (isQuotedMessage) { + parts.push(partCreator.plain(QUOTE_LINE_PREFIX)); + } + parts.push(...parseAtRoomMentions(line, partCreator)); + return parts; + }, []); + return parts; +} + +export function parseEvent(event, partCreator, {isQuotedMessage = false}) { const content = event.getContent(); let parts; if (content.format === "org.matrix.custom.html") { - parts = parseHtmlMessage(content.formatted_body || "", partCreator); + parts = parseHtmlMessage(content.formatted_body || "", partCreator, isQuotedMessage); } else { - const body = content.body || ""; - const lines = body.split("\n"); - parts = lines.reduce((parts, line, i) => { - const isLast = i === lines.length - 1; - const newParts = parseAtRoomMentions(line, partCreator); - if (!isLast) { - newParts.push(partCreator.newline()); - } - return parts.concat(newParts); - }, []); + parts = parsePlainTextMessage(content.body || "", partCreator, isQuotedMessage); } if (content.msgtype === "m.emote") { parts.unshift(partCreator.plain("/me ")); From ce44c651d04afd1cdf6f653b4116e82187f30975 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 12:35:29 +0200 Subject: [PATCH 191/413] keep deserialized parts compatible with part api, to avoid breakage when passing real parts --- src/editor/parts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/parts.js b/src/editor/parts.js index d3569a7347..cf11dc74dc 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -312,7 +312,7 @@ class UserPillPart extends PillPart { serialize() { const obj = super.serialize(); - obj.userId = this.resourceId; + obj.resourceId = this.resourceId; return obj; } } @@ -403,7 +403,7 @@ export class PartCreator { case "room-pill": return this.roomPill(part.text); case "user-pill": - return this.userPill(part.text, part.userId); + return this.userPill(part.text, part.resourceId); } } From 10c218825bc4b7033be989c28576003e9a8dfad0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 12:36:05 +0200 Subject: [PATCH 192/413] allow inserting multiple parts at a position --- src/components/views/rooms/SendMessageComposer.js | 2 +- src/editor/model.js | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 916f93d544..b3c65b5a97 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -134,7 +134,7 @@ export default class SendMessageComposer extends React.Component { const displayName = member ? member.rawDisplayName : payload.user_id; const userPillPart = this.model.partCreator.userPill(displayName, userId); - this.model.insertPartAt(userPillPart, this._editorRef.getCaret()); + this.model.insertPartsAt([userPillPart], this._editorRef.getCaret()); break; } } diff --git a/src/editor/model.js b/src/editor/model.js index 3a8c02da1b..99d590bf26 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -108,12 +108,18 @@ export default class EditorModel { this._updateCallback(caret, inputType); } - insertPartAt(part, caret) { + insertPartsAt(parts, caret) { const position = this.positionForOffset(caret.offset, caret.atNodeEnd); const insertIndex = this._splitAt(position); - this._insertPart(insertIndex, part); - // want to put caret after new part? - const newPosition = new DocumentPosition(insertIndex, part.text.length); + let newTextLength = 0; + for (let i = 0; i < parts.length; ++i) { + const part = parts[i]; + newTextLength += part.text.length; + this._insertPart(insertIndex + i, part); + } + // put caret after new part + const lastPartIndex = insertIndex + parts.length - 1; + const newPosition = new DocumentPosition(lastPartIndex, newTextLength); this._updateCallback(newPosition); } From 60e10364b0642a477e3fefeb213ec412e6125174 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 12:36:19 +0200 Subject: [PATCH 193/413] add quoting functionality to new composer --- src/components/views/rooms/SendMessageComposer.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index b3c65b5a97..259e32682a 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -26,6 +26,7 @@ import BasicMessageComposer from "./BasicMessageComposer"; import ReplyPreview from "./ReplyPreview"; import RoomViewStore from '../../../stores/RoomViewStore'; import ReplyThread from "../elements/ReplyThread"; +import {parseEvent} from '../../../editor/deserialize'; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -137,6 +138,17 @@ export default class SendMessageComposer extends React.Component { this.model.insertPartsAt([userPillPart], this._editorRef.getCaret()); break; } + case 'quote': { + const {partCreator} = this.model; + const quoteParts = parseEvent(payload.event, partCreator, { isQuotedMessage: true }); + // add two newlines + quoteParts.push(partCreator.newline()); + quoteParts.push(partCreator.newline()); + this.model.insertPartsAt(quoteParts, {offset: 0}); + // refocus on composer, as we just clicked "Quote" + this._editorRef.focus(); + break; + } } }; From 2e71dd3ea8d15ce5e785e04942b49acf8410d7e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 13:51:06 +0200 Subject: [PATCH 194/413] cleanup, move code out of big switch statement --- .../views/rooms/SendMessageComposer.js | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 259e32682a..167615c891 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -129,29 +129,36 @@ export default class SendMessageComposer extends React.Component { case 'focus_composer': this._editorRef.focus(); break; - case 'insert_mention': { - const userId = payload.user_id; - const member = this.props.room.getMember(userId); - const displayName = member ? - member.rawDisplayName : payload.user_id; - const userPillPart = this.model.partCreator.userPill(displayName, userId); - this.model.insertPartsAt([userPillPart], this._editorRef.getCaret()); + case 'insert_mention': + this._insertMention(payload.user_id); break; - } - case 'quote': { - const {partCreator} = this.model; - const quoteParts = parseEvent(payload.event, partCreator, { isQuotedMessage: true }); - // add two newlines - quoteParts.push(partCreator.newline()); - quoteParts.push(partCreator.newline()); - this.model.insertPartsAt(quoteParts, {offset: 0}); - // refocus on composer, as we just clicked "Quote" - this._editorRef.focus(); + case 'quote': + this._insertQuotedMessage(payload.event); break; - } } }; + _insertMention(userId) { + const member = this.props.room.getMember(userId); + const displayName = member ? + member.rawDisplayName : userId; + const userPillPart = this.model.partCreator.userPill(displayName, userId); + this.model.insertPartsAt([userPillPart], this._editorRef.getCaret()); + // refocus on composer, as we just clicked "Mention" + this._editorRef.focus(); + } + + _insertQuotedMessage(event) { + const {partCreator} = this.model; + const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true }); + // add two newlines + quoteParts.push(partCreator.newline()); + quoteParts.push(partCreator.newline()); + this.model.insertPartsAt(quoteParts, {offset: 0}); + // refocus on composer, as we just clicked "Quote" + this._editorRef.focus(); + } + render() { return (
From 9003a8836a9be95753277d6820fe256cd92a5a5b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 13:56:11 +0200 Subject: [PATCH 195/413] put dispatches together --- .../views/rooms/SendMessageComposer.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 167615c891..4c6d763411 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -41,13 +41,6 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { } content.body = nestedReply.body + content.body; } - - // Clear reply_to_event as we put the message into the queue - // if the send fails, retry will handle resending. - dis.dispatch({ - action: 'reply_to_event', - event: null, - }); } function createMessageContent(model, permalinkCreator) { @@ -103,9 +96,18 @@ export default class SendMessageComposer extends React.Component { } _sendMessage() { + const isReply = !!RoomViewStore.getQuotingEvent(); const {roomId} = this.props.room; this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model, this.props.permalinkCreator)); this.model.reset([]); + if (isReply) { + // Clear reply_to_event as we put the message into the queue + // if the send fails, retry will handle resending. + dis.dispatch({ + action: 'reply_to_event', + event: null, + }); + } dis.dispatch({action: 'focus_composer'}); } From ea1faacd8bc5178026fe5bcf138bfaa5e6670e86 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 14:14:07 +0200 Subject: [PATCH 196/413] support alt+enter on mac, like slate composer --- src/components/views/rooms/BasicMessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 62e136000f..9ada9df720 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -186,7 +186,7 @@ export default class BasicMessageEditor extends React.Component { } handled = true; // insert newline on Shift+Enter - } else if (event.shiftKey && event.key === "Enter") { + } else if (event.key === "Enter" && (event.shiftKey || (IS_MAC && event.altKey))) { this._insertText("\n"); handled = true; // autocomplete or enter to send below shouldn't have any modifier keys pressed. From 0adca10f9fd4cad7217e828882d74ac09856d1a0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 17:15:12 +0200 Subject: [PATCH 197/413] make named options argument optional --- src/editor/deserialize.js | 2 +- src/editor/serialize.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 08827ca89a..d5efef5d1a 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -236,7 +236,7 @@ function parsePlainTextMessage(body, partCreator, isQuotedMessage) { return parts; } -export function parseEvent(event, partCreator, {isQuotedMessage = false}) { +export function parseEvent(event, partCreator, {isQuotedMessage = false} = {}) { const content = event.getContent(); let parts; if (content.format === "org.matrix.custom.html") { diff --git a/src/editor/serialize.js b/src/editor/serialize.js index cb06eede6c..0746f6788e 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -33,7 +33,7 @@ export function mdSerialize(model) { }, ""); } -export function htmlSerializeIfNeeded(model, {forceHTML = false}) { +export function htmlSerializeIfNeeded(model, {forceHTML = false} = {}) { const md = mdSerialize(model); const parser = new Markdown(md); if (!parser.isPlainText() || forceHTML) { From 9bc8ff7e1eb98cda655bd1cadff8bfebfec09f6d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 17:15:52 +0200 Subject: [PATCH 198/413] clear composer undo history when sending a message --- src/components/views/rooms/BasicMessageComposer.js | 4 ++++ src/components/views/rooms/SendMessageComposer.js | 2 ++ src/editor/history.js | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 9ada9df720..a488990210 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -136,6 +136,10 @@ export default class BasicMessageEditor extends React.Component { return this._lastCaret; } + clearUndoHistory() { + this.historyManager.clear(); + } + getCaret() { return this._lastCaret; } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 4c6d763411..3b65b3e83d 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -100,6 +100,8 @@ export default class SendMessageComposer extends React.Component { const {roomId} = this.props.room; this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model, this.props.permalinkCreator)); this.model.reset([]); + this._editorRef.clearUndoHistory(); + if (isReply) { // Clear reply_to_event as we put the message into the queue // if the send fails, retry will handle resending. diff --git a/src/editor/history.js b/src/editor/history.js index 6fd67d067c..de052cf682 100644 --- a/src/editor/history.js +++ b/src/editor/history.js @@ -18,6 +18,10 @@ export const MAX_STEP_LENGTH = 10; export default class HistoryManager { constructor() { + this.clear(); + } + + clear() { this._stack = []; this._newlyTypedCharCount = 0; this._currentIndex = -1; From ca3539d53e6b6f8e1181b71305f61b190eb5401e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 17:16:12 +0200 Subject: [PATCH 199/413] remove dead code --- src/components/views/rooms/BasicMessageComposer.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index a488990210..ccee439237 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -220,11 +220,6 @@ export default class BasicMessageEditor extends React.Component { } } - _cancelEdit = () => { - dis.dispatch({action: "edit_event", event: null}); - dis.dispatch({action: 'focus_composer'}); - } - isModified() { return this._modifiedFlag; } From cc82353d8fa2a4f65ea66b801d9c328dce1a55fd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Aug 2019 17:18:46 +0200 Subject: [PATCH 200/413] bring back composer send history and arrow up to edit previous message --- src/ComposerHistoryManager.js | 56 ++++----------- .../views/rooms/BasicMessageComposer.js | 4 ++ .../views/rooms/SendMessageComposer.js | 70 +++++++++++++++++-- 3 files changed, 84 insertions(+), 46 deletions(-) diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js index 1b3fb588eb..33030ed6cf 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -15,38 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Value} from 'slate'; - import _clamp from 'lodash/clamp'; -type MessageFormat = 'rich' | 'markdown'; - -class HistoryItem { - // We store history items in their native format to ensure history is accurate - // and then convert them if our RTE has subsequently changed format. - value: Value; - format: MessageFormat = 'rich'; - - constructor(value: ?Value, format: ?MessageFormat) { - this.value = value; - this.format = format; - } - - static fromJSON(obj: Object): HistoryItem { - return new HistoryItem( - Value.fromJSON(obj.value), - obj.format, - ); - } - - toJSON(): Object { - return { - value: this.value.toJSON(), - format: this.format, - }; - } -} - export default class ComposerHistoryManager { history: Array = []; prefix: string; @@ -57,26 +27,30 @@ export default class ComposerHistoryManager { this.prefix = prefix + roomId; // TODO: Performance issues? - let item; - for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { + let index = 0; + let itemJSON; + + while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) { try { - this.history.push( - HistoryItem.fromJSON(JSON.parse(item)), - ); + const serializedParts = JSON.parse(itemJSON); + this.history.push(serializedParts); } catch (e) { console.warn("Throwing away unserialisable history", e); + break; } + ++index; } - this.lastIndex = this.currentIndex; + this.lastIndex = this.history.length - 1; // reset currentIndex to account for any unserialisable history - this.currentIndex = this.history.length; + this.currentIndex = this.lastIndex + 1; } - save(value: Value, format: MessageFormat) { - const item = new HistoryItem(value, format); - this.history.push(item); + save(editorModel: Object) { + const serializedParts = editorModel.serializeParts(); + this.history.push(serializedParts); this.currentIndex = this.history.length; - sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); + this.lastIndex += 1; + sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(serializedParts)); } getItem(offset: number): ?HistoryItem { diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index ccee439237..92abefa117 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -144,6 +144,10 @@ export default class BasicMessageEditor extends React.Component { return this._lastCaret; } + isSelectionCollapsed() { + return !this._lastSelection || this._lastSelection.isCollapsed; + } + isCaretAtStart() { return this.getCaret().offset === 0; } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 3b65b3e83d..a400433aef 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -27,6 +27,8 @@ import ReplyPreview from "./ReplyPreview"; import RoomViewStore from '../../../stores/RoomViewStore'; import ReplyThread from "../elements/ReplyThread"; import {parseEvent} from '../../../editor/deserialize'; +import {findEditableEvent} from '../../../utils/EventUtils'; +import ComposerHistoryManager from "../../../ComposerHistoryManager"; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -79,6 +81,7 @@ export default class SendMessageComposer extends React.Component { super(props, context); this.model = null; this._editorRef = null; + this.currentlyComposedEditorState = null; } _setEditorRef = ref => { @@ -86,19 +89,75 @@ export default class SendMessageComposer extends React.Component { }; _onKeyDown = (event) => { - if (event.metaKey || event.altKey || event.shiftKey) { - return; - } - if (event.key === "Enter") { + const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; + if (event.key === "Enter" && !hasModifier) { this._sendMessage(); event.preventDefault(); + } else if (event.key === "ArrowUp") { + this.onVerticalArrow(event, true); + } else if (event.key === "ArrowDown") { + this.onVerticalArrow(event, false); + } + } + + onVerticalArrow(e, up) { + if (e.ctrlKey || e.shiftKey || e.metaKey) return; + + const shouldSelectHistory = e.altKey; + const shouldEditLastMessage = !e.altKey && up && !RoomViewStore.getQuotingEvent(); + + if (shouldSelectHistory) { + // Try select composer history + const selected = this.selectSendHistory(up); + if (selected) { + // We're selecting history, so prevent the key event from doing anything else + e.preventDefault(); + } + } else if (shouldEditLastMessage) { + // selection must be collapsed and caret at start + if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) { + const editEvent = findEditableEvent(this.props.room, false); + if (editEvent) { + // We're selecting history, so prevent the key event from doing anything else + e.preventDefault(); + dis.dispatch({ + action: 'edit_event', + event: editEvent, + }); + } + } + } + } + + selectSendHistory(up) { + const delta = up ? -1 : 1; + + // True if we are not currently selecting history, but composing a message + if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) { + // We can't go any further - there isn't any more history, so nop. + if (!up) { + return; + } + this.currentlyComposedEditorState = this.model.serializeParts(); + } else if (this.sendHistoryManager.currentIndex + delta === this.sendHistoryManager.history.length) { + // True when we return to the message being composed currently + this.model.reset(this.currentlyComposedEditorState); + this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length; + return; + } + const serializedParts = this.sendHistoryManager.getItem(delta); + if (serializedParts) { + this.model.reset(serializedParts); + this._editorRef.focus(); } } _sendMessage() { const isReply = !!RoomViewStore.getQuotingEvent(); const {roomId} = this.props.room; - this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model, this.props.permalinkCreator)); + const content = createMessageContent(this.model, this.props.permalinkCreator); + this.context.matrixClient.sendMessage(roomId, content); + this.sendHistoryManager.save(this.model); this.model.reset([]); this._editorRef.clearUndoHistory(); @@ -125,6 +184,7 @@ export default class SendMessageComposer extends React.Component { const partCreator = new PartCreator(this.props.room, this.context.matrixClient); this.model = new EditorModel([], partCreator); this.dispatcherRef = dis.register(this.onAction); + this.sendHistoryManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } onAction = (payload) => { From 88cc1c428dd5df6318228bd0b3ec836f8876694c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Aug 2019 11:26:21 +0200 Subject: [PATCH 201/413] add support for emotes and running /commands this does not yet include autocomplete for commands --- .../views/rooms/EditMessageComposer.js | 13 +-- .../views/rooms/SendMessageComposer.js | 88 +++++++++++++++---- src/editor/serialize.js | 12 +++ 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 7330405b81..d58279436d 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; import {getCaretOffsetAndText} from '../../../editor/dom'; -import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize'; +import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; import {findEditableEvent} from '../../../utils/EventUtils'; import {parseEvent} from '../../../editor/deserialize'; import {PartCreator} from '../../../editor/parts'; @@ -56,17 +56,10 @@ function getTextReplyFallback(mxEvent) { return ""; } -function _isEmote(model) { - const firstPart = model.parts[0]; - return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me "); -} - function createEditContent(model, editedEvent) { - const isEmote = _isEmote(model); + const isEmote = containsEmote(model); if (isEmote) { - // trim "/me " - model = model.clone(); - model.removeText({index: 0, offset: 0}, 4); + model = stripEmoteCommand(model); } const isReply = _isReply(editedEvent); let plainPrefix = ""; diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index a400433aef..bd5b5b9102 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; import {getCaretOffsetAndText} from '../../../editor/dom'; -import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize'; +import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; import {PartCreator} from '../../../editor/parts'; import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; @@ -29,6 +29,10 @@ import ReplyThread from "../elements/ReplyThread"; import {parseEvent} from '../../../editor/deserialize'; import {findEditableEvent} from '../../../utils/EventUtils'; import ComposerHistoryManager from "../../../ComposerHistoryManager"; +import {processCommandInput} from '../../../SlashCommands'; +import sdk from '../../../index'; +import Modal from '../../../Modal'; +import { _t } from '../../../languageHandler'; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -46,11 +50,15 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { } function createMessageContent(model, permalinkCreator) { + const isEmote = containsEmote(model); + if (isEmote) { + model = stripEmoteCommand(model); + } const repliedToEvent = RoomViewStore.getQuotingEvent(); const body = textSerialize(model); const content = { - msgtype: "m.text", + msgtype: isEmote ? "m.emote" : "m.text", body: body, }; const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!repliedToEvent}); @@ -129,9 +137,10 @@ export default class SendMessageComposer extends React.Component { } } + // we keep sent messages/commands in a separate history (separate from undo history) + // so you can alt+up/down in them selectSendHistory(up) { const delta = up ? -1 : 1; - // True if we are not currently selecting history, but composing a message if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) { // We can't go any further - there isn't any more history, so nop. @@ -152,24 +161,69 @@ export default class SendMessageComposer extends React.Component { } } + _isSlashCommand() { + const parts = this.model.parts; + const isPlain = parts.reduce((isPlain, part) => { + return isPlain && (part.type === "plain" || part.type === "newline"); + }, true); + return isPlain && parts.length > 0 && parts[0].text.startsWith("/"); + } + + async _runSlashCommand() { + const commandText = this.model.parts.reduce((text, part) => { + return text + part.text; + }, ""); + const cmd = processCommandInput(this.props.room.roomId, commandText); + + if (cmd) { + let error = cmd.error; + if (cmd.promise) { + try { + await cmd.promise; + } catch (err) { + error = err; + } + } + if (error) { + console.error("Command failure: %s", error); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + // assume the error is a server error when the command is async + const isServerError = !!cmd.promise; + const title = isServerError ? "Server error" : "Command error"; + Modal.createTrackedDialog(title, '', ErrorDialog, { + title: isServerError ? _t("Server error") : _t("Command error"), + description: error.message ? error.message : _t( + "Server unavailable, overloaded, or something else went wrong.", + ), + }); + } else { + console.log("Command success."); + } + } + } + _sendMessage() { - const isReply = !!RoomViewStore.getQuotingEvent(); - const {roomId} = this.props.room; - const content = createMessageContent(this.model, this.props.permalinkCreator); - this.context.matrixClient.sendMessage(roomId, content); + if (!containsEmote(this.model) && this._isSlashCommand()) { + this._runSlashCommand(); + } else { + const isReply = !!RoomViewStore.getQuotingEvent(); + const {roomId} = this.props.room; + const content = createMessageContent(this.model, this.props.permalinkCreator); + this.context.matrixClient.sendMessage(roomId, content); + if (isReply) { + // Clear reply_to_event as we put the message into the queue + // if the send fails, retry will handle resending. + dis.dispatch({ + action: 'reply_to_event', + event: null, + }); + } + } this.sendHistoryManager.save(this.model); + // clear composer this.model.reset([]); this._editorRef.clearUndoHistory(); - - if (isReply) { - // Clear reply_to_event as we put the message into the queue - // if the send fails, retry will handle resending. - dis.dispatch({ - action: 'reply_to_event', - event: null, - }); - } - dis.dispatch({action: 'focus_composer'}); + this._editorRef.focus(); } componentWillUnmount() { diff --git a/src/editor/serialize.js b/src/editor/serialize.js index 0746f6788e..756a27dd03 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -56,3 +56,15 @@ export function textSerialize(model) { } }, ""); } + +export function containsEmote(model) { + const firstPart = model.parts[0]; + return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me "); +} + +export function stripEmoteCommand(model) { + // trim "/me " + model = model.clone(); + model.removeText({index: 0, offset: 0}, 4); + return model; +} From c5cd8b943aa20471c58f15ec4ce86f30354343e9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Aug 2019 15:27:50 +0200 Subject: [PATCH 202/413] support auto complete for /commands --- .../views/rooms/SendMessageComposer.js | 10 ++----- src/editor/model.js | 2 +- src/editor/parts.js | 30 +++++++++++++++++++ src/editor/serialize.js | 8 ++++- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index bd5b5b9102..da858e9029 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -20,7 +20,7 @@ import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; import {getCaretOffsetAndText} from '../../../editor/dom'; import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; -import {PartCreator} from '../../../editor/parts'; +import {CommandPartCreator} from '../../../editor/parts'; import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyPreview from "./ReplyPreview"; @@ -164,7 +164,7 @@ export default class SendMessageComposer extends React.Component { _isSlashCommand() { const parts = this.model.parts; const isPlain = parts.reduce((isPlain, part) => { - return isPlain && (part.type === "plain" || part.type === "newline"); + return isPlain && (part.type === "command" || part.type === "plain" || part.type === "newline"); }, true); return isPlain && parts.length > 0 && parts[0].text.startsWith("/"); } @@ -227,15 +227,11 @@ export default class SendMessageComposer extends React.Component { } componentWillUnmount() { - const sel = document.getSelection(); - const {caret} = getCaretOffsetAndText(this._editorRef, sel); - const parts = this.model.serializeParts(); - this.props.editState.setEditorState(caret, parts); dis.unregister(this.dispatcherRef); } componentWillMount() { - const partCreator = new PartCreator(this.props.room, this.context.matrixClient); + const partCreator = new CommandPartCreator(this.props.room, this.context.matrixClient); this.model = new EditorModel([], partCreator); this.dispatcherRef = dis.register(this.onAction); this.sendHistoryManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); diff --git a/src/editor/model.js b/src/editor/model.js index 99d590bf26..91b724cf9e 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -303,7 +303,7 @@ export default class EditorModel { index = 0; } while (str) { - const newPart = this._partCreator.createPartForInput(str); + const newPart = this._partCreator.createPartForInput(str, index); if (validate) { str = newPart.appendUntilRejected(str); } else { diff --git a/src/editor/parts.js b/src/editor/parts.js index cf11dc74dc..f9b4243de4 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -441,3 +441,33 @@ export class PartCreator { } } +// part creator that support auto complete for /commands, +// used in SendMessageComposer +export class CommandPartCreator extends PartCreator { + createPartForInput(text, partIndex) { + // at beginning and starts with /? create + if (partIndex === 0 && text[0] === "/") { + return new CommandPart("", this._autoCompleteCreator); + } else { + return super.createPartForInput(text, partIndex); + } + } + + deserializePart(part) { + if (part.type === "command") { + return new CommandPart(part.text, this._autoCompleteCreator); + } else { + return super.deserializePart(part); + } + } +} + +class CommandPart extends PillCandidatePart { + acceptsInsertion(chr, i) { + return PlainPart.prototype.acceptsInsertion.call(this, chr, i); + } + + get type() { + return "command"; + } +} diff --git a/src/editor/serialize.js b/src/editor/serialize.js index 756a27dd03..5a1a941309 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -23,6 +23,7 @@ export function mdSerialize(model) { case "newline": return html + "\n"; case "plain": + case "command": case "pill-candidate": case "at-room-pill": return html + part.text; @@ -47,6 +48,7 @@ export function textSerialize(model) { case "newline": return text + "\n"; case "plain": + case "command": case "pill-candidate": case "at-room-pill": return text + part.text; @@ -59,7 +61,11 @@ export function textSerialize(model) { export function containsEmote(model) { const firstPart = model.parts[0]; - return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me "); + // part type will be "plain" while editing, + // and "command" while composing a message. + return firstPart && + (firstPart.type === "plain" || firstPart.type === "command") && + firstPart.text.startsWith("/me "); } export function stripEmoteCommand(model) { From 3c5cf3e778c61722bdaaa00841cc31a9031ce3d1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Aug 2019 15:34:49 +0200 Subject: [PATCH 203/413] rename ComposerHistoryManager to SendHistoryManager to avoid confusion ...with the undo history manager for the composer. --- src/{ComposerHistoryManager.js => SendHistoryManager.js} | 2 +- src/components/views/rooms/SendMessageComposer.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) rename src/{ComposerHistoryManager.js => SendHistoryManager.js} (97%) diff --git a/src/ComposerHistoryManager.js b/src/SendHistoryManager.js similarity index 97% rename from src/ComposerHistoryManager.js rename to src/SendHistoryManager.js index 33030ed6cf..c838f1246b 100644 --- a/src/ComposerHistoryManager.js +++ b/src/SendHistoryManager.js @@ -17,7 +17,7 @@ limitations under the License. import _clamp from 'lodash/clamp'; -export default class ComposerHistoryManager { +export default class SendHistoryManager { history: Array = []; prefix: string; lastIndex: number = 0; // used for indexing the storage diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index da858e9029..0d1d24c282 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -18,7 +18,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; -import {getCaretOffsetAndText} from '../../../editor/dom'; import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; import {CommandPartCreator} from '../../../editor/parts'; import {MatrixClient} from 'matrix-js-sdk'; @@ -28,7 +27,7 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import ReplyThread from "../elements/ReplyThread"; import {parseEvent} from '../../../editor/deserialize'; import {findEditableEvent} from '../../../utils/EventUtils'; -import ComposerHistoryManager from "../../../ComposerHistoryManager"; +import SendHistoryManager from "../../../SendHistoryManager"; import {processCommandInput} from '../../../SlashCommands'; import sdk from '../../../index'; import Modal from '../../../Modal'; @@ -234,7 +233,7 @@ export default class SendMessageComposer extends React.Component { const partCreator = new CommandPartCreator(this.props.room, this.context.matrixClient); this.model = new EditorModel([], partCreator); this.dispatcherRef = dis.register(this.onAction); - this.sendHistoryManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); + this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } onAction = (payload) => { From e2dfe888ccae21e8eeec83686fa374cc24704967 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Aug 2019 16:22:04 +0200 Subject: [PATCH 204/413] only capture Enter when autocompletion list has selection this is the old behaviour and makes sense IMO also close the auto complete when resetting the composer model, in case it was still open --- .../views/rooms/BasicMessageComposer.js | 25 ++++++++++++++----- src/editor/autocomplete.js | 6 ++++- src/editor/model.js | 7 ++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 92abefa117..d4b9cd87c9 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -203,19 +203,32 @@ export default class BasicMessageEditor extends React.Component { const autoComplete = model.autoComplete; switch (event.key) { case "Enter": - autoComplete.onEnter(event); break; + // only capture enter when something is selected in the list, + // otherwise don't handle so the contents of the composer gets sent + if (autoComplete.hasSelection()) { + autoComplete.onEnter(event); + handled = true; + } + break; case "ArrowUp": - autoComplete.onUpArrow(event); break; + autoComplete.onUpArrow(event); + handled = true; + break; case "ArrowDown": - autoComplete.onDownArrow(event); break; + autoComplete.onDownArrow(event); + handled = true; + break; case "Tab": - autoComplete.onTab(event); break; + autoComplete.onTab(event); + handled = true; + break; case "Escape": - autoComplete.onEscape(event); break; + autoComplete.onEscape(event); + handled = true; + break; default: return; // don't preventDefault on anything else } - handled = true; } } if (handled) { diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index 2aedf8d7f5..ac662c32d8 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -33,6 +33,10 @@ export default class AutocompleteWrapperModel { }); } + hasSelection() { + return this._getAutocompleterComponent().hasSelection(); + } + onEnter() { this._updateCallback({close: true}); } @@ -103,7 +107,7 @@ export default class AutocompleteWrapperModel { } case "#": return this._partCreator.roomPill(completionId); - // also used for emoji completion + // used for emoji and command completion replacement default: return this._partCreator.plain(text); } diff --git a/src/editor/model.js b/src/editor/model.js index 91b724cf9e..2f1e5218d8 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -105,6 +105,13 @@ export default class EditorModel { reset(serializedParts, caret, inputType) { this._parts = serializedParts.map(p => this._partCreator.deserializePart(p)); + // close auto complete if open + // this would happen when clearing the composer after sending + // a message with the autocomplete still open + if (this._autoComplete) { + this._autoComplete = null; + this._autoCompletePartIdx = null; + } this._updateCallback(caret, inputType); } From 6df46cc319b5b95cc32437920328d6c048d4b339 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Aug 2019 16:40:16 +0200 Subject: [PATCH 205/413] send typing notifs in new composer (both send and edit) --- src/components/views/rooms/BasicMessageComposer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index d4b9cd87c9..19111f1f45 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; -import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; import {setCaretPosition} from '../../../editor/caret'; @@ -25,6 +24,7 @@ import Autocomplete from '../rooms/Autocomplete'; import {autoCompleteCreator} from '../../../editor/parts'; import {renderModel} from '../../../editor/render'; import {Room} from 'matrix-js-sdk'; +import TypingStore from "../../../stores/TypingStore"; const IS_MAC = navigator.platform.indexOf("Mac") !== -1; @@ -90,6 +90,7 @@ export default class BasicMessageEditor extends React.Component { } this.setState({autoComplete: this.props.model.autoComplete}); this.historyManager.tryPush(this.props.model, caret, inputType, diff); + TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty); } _onInput = (event) => { From 9f72268df7c72f79e0e4983700a6dd5e640d2efa Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Aug 2019 16:40:35 +0200 Subject: [PATCH 206/413] avoid null-refs when receiving an action before initial rendering --- src/components/views/rooms/SendMessageComposer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 0d1d24c282..4137de54c5 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -240,7 +240,7 @@ export default class SendMessageComposer extends React.Component { switch (payload.action) { case 'reply_to_event': case 'focus_composer': - this._editorRef.focus(); + this._editorRef && this._editorRef.focus(); break; case 'insert_mention': this._insertMention(payload.user_id); @@ -258,7 +258,7 @@ export default class SendMessageComposer extends React.Component { const userPillPart = this.model.partCreator.userPill(displayName, userId); this.model.insertPartsAt([userPillPart], this._editorRef.getCaret()); // refocus on composer, as we just clicked "Mention" - this._editorRef.focus(); + this._editorRef && this._editorRef.focus(); } _insertQuotedMessage(event) { @@ -269,7 +269,7 @@ export default class SendMessageComposer extends React.Component { quoteParts.push(partCreator.newline()); this.model.insertPartsAt(quoteParts, {offset: 0}); // refocus on composer, as we just clicked "Quote" - this._editorRef.focus(); + this._editorRef && this._editorRef.focus(); } render() { From b366b0b3d845b5e4d0cc43933d398874dc55c751 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Aug 2019 17:42:09 +0200 Subject: [PATCH 207/413] store composer state when typing in new composer this doesn't use the MessageComposerStore on purpose so that both the new and old composer don't overwrite each others state, as the format is different. --- .../views/rooms/BasicMessageComposer.js | 5 ++++ .../views/rooms/SendMessageComposer.js | 30 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 19111f1f45..e4179d9c3b 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -52,6 +52,7 @@ function selectionEquals(a: Selection, b: Selection): boolean { export default class BasicMessageEditor extends React.Component { static propTypes = { + onChange: PropTypes.func, model: PropTypes.instanceOf(EditorModel).isRequired, room: PropTypes.instanceOf(Room).isRequired, placeholder: PropTypes.string, @@ -91,6 +92,10 @@ export default class BasicMessageEditor extends React.Component { this.setState({autoComplete: this.props.model.autoComplete}); this.historyManager.tryPush(this.props.model, caret, inputType, diff); TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty); + + if (this.props.onChange) { + this.props.onChange(); + } } _onInput = (event) => { diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 4137de54c5..7d831dad4a 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -223,6 +223,7 @@ export default class SendMessageComposer extends React.Component { this.model.reset([]); this._editorRef.clearUndoHistory(); this._editorRef.focus(); + this._clearStoredEditorState(); } componentWillUnmount() { @@ -231,11 +232,37 @@ export default class SendMessageComposer extends React.Component { componentWillMount() { const partCreator = new CommandPartCreator(this.props.room, this.context.matrixClient); - this.model = new EditorModel([], partCreator); + const parts = this._restoreStoredEditorState(partCreator) || []; + this.model = new EditorModel(parts, partCreator); this.dispatcherRef = dis.register(this.onAction); this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } + get _editorStateKey() { + return `cider_editor_state_${this.props.room.roomId}`; + } + + _clearStoredEditorState() { + localStorage.removeItem(this._editorStateKey); + } + + _restoreStoredEditorState(partCreator) { + const json = localStorage.getItem(this._editorStateKey); + if (json) { + const serializedParts = JSON.parse(json); + const parts = serializedParts.map(p => partCreator.deserializePart(p)); + return parts; + } + } + + _saveStoredEditorState = () => { + if (this.model.isEmpty) { + this._clearStoredEditorState(); + } else { + localStorage.setItem(this._editorStateKey, JSON.stringify(this.model.serializeParts())); + } + } + onAction = (payload) => { switch (payload.action) { case 'reply_to_event': @@ -284,6 +311,7 @@ export default class SendMessageComposer extends React.Component { room={this.props.room} label={this.props.placeholder} placeholder={this.props.placeholder} + onChange={this._saveStoredEditorState} />
); From 36390da634bd6408afa549722c91594bcced0217 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Aug 2019 17:43:26 +0200 Subject: [PATCH 208/413] some doc improvements --- docs/ciderEditor.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index 2448be852a..e67c74a95c 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -17,7 +17,7 @@ The parts are then reconciled with the DOM. When typing in the `contenteditable` element, the `input` event fires and the DOM of the editor is turned into a string. The way this is done has some logic to it to deal with adding newlines for block elements, to make sure -the caret offset is calculated in the same way as the content string, and the ignore +the caret offset is calculated in the same way as the content string, and to ignore caret nodes (more on that later). For these reasons it doesn't use `innerText`, `textContent` or anything similar. The model addresses any content in the editor within as an offset within this string. @@ -25,13 +25,13 @@ The caret position is thus also converted from a position in the DOM tree to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.js`. Once the content string and caret offset is calculated, it is passed to the `update()` -method of the model. The model first calculates the same content string its current parts, +method of the model. The model first calculates the same content string of its current parts, basically just concatenating their text. It then looks for differences between the current and the new content string. The diffing algorithm is very basic, and assumes there is only one change around the caret offset, so this should be very inexpensive. See `diff.js` for details. -The result of the diffing is the strings that was added and/or removed from +The result of the diffing is the strings that were added and/or removed from the current content. These differences are then applied to the parts, where parts can apply validation logic to these changes. @@ -48,7 +48,8 @@ to leave the parts it intersects alone. The benefit of this is that we can use the `input` event, which is broadly supported, to find changes in the editor. We don't have to rely on keyboard events, -which relate poorly to text input or changes. +which relate poorly to text input or changes, and don't need the `beforeinput` event, +which isn't broadly supported yet. Once the parts of the model are updated, the DOM of the editor is then reconciled with the new model state, see `renderModel` in `render.js` for this. From 6a3ecde4e6b3fd515738a5a93ccb51b5a7bfa4a0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Aug 2019 14:05:54 +0100 Subject: [PATCH 209/413] duplicate slate code where we changed it drastically to still make it work when the feature flag will be turned off --- .eslintignore.errorfiles | 2 +- src/SlateComposerHistoryManager.js | 86 +++ .../views/rooms/MessageComposerInput.js | 6 +- .../views/rooms/SlateMessageComposer.js | 489 ++++++++++++++++++ 4 files changed, 579 insertions(+), 4 deletions(-) create mode 100644 src/SlateComposerHistoryManager.js create mode 100644 src/components/views/rooms/SlateMessageComposer.js diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index c129f801a1..02629ea169 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -34,7 +34,7 @@ src/components/views/rooms/LinkPreviewWidget.js src/components/views/rooms/MemberDeviceInfo.js src/components/views/rooms/MemberInfo.js src/components/views/rooms/MemberList.js -src/components/views/rooms/MessageComposer.js +src/components/views/rooms/SlateMessageComposer.js src/components/views/rooms/PinnedEventTile.js src/components/views/rooms/RoomList.js src/components/views/rooms/RoomPreviewBar.js diff --git a/src/SlateComposerHistoryManager.js b/src/SlateComposerHistoryManager.js new file mode 100644 index 0000000000..948dcf64ff --- /dev/null +++ b/src/SlateComposerHistoryManager.js @@ -0,0 +1,86 @@ +//@flow +/* +Copyright 2017 Aviral Dasgupta + +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 {Value} from 'slate'; + +import _clamp from 'lodash/clamp'; + +type MessageFormat = 'rich' | 'markdown'; + +class HistoryItem { + // We store history items in their native format to ensure history is accurate + // and then convert them if our RTE has subsequently changed format. + value: Value; + format: MessageFormat = 'rich'; + + constructor(value: ?Value, format: ?MessageFormat) { + this.value = value; + this.format = format; + } + + static fromJSON(obj: Object): HistoryItem { + return new HistoryItem( + Value.fromJSON(obj.value), + obj.format, + ); + } + + toJSON(): Object { + return { + value: this.value.toJSON(), + format: this.format, + }; + } +} + +export default class SlateComposerHistoryManager { + history: Array = []; + prefix: string; + lastIndex: number = 0; // used for indexing the storage + currentIndex: number = 0; // used for indexing the loaded validated history Array + + constructor(roomId: string, prefix: string = 'mx_composer_history_') { + this.prefix = prefix + roomId; + + // TODO: Performance issues? + let item; + for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { + try { + this.history.push( + HistoryItem.fromJSON(JSON.parse(item)), + ); + } catch (e) { + console.warn("Throwing away unserialisable history", e); + } + } + this.lastIndex = this.currentIndex; + // reset currentIndex to account for any unserialisable history + this.currentIndex = this.history.length; + } + + save(value: Value, format: MessageFormat) { + const item = new HistoryItem(value, format); + this.history.push(item); + this.currentIndex = this.history.length; + sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); + } + + getItem(offset: number): ?HistoryItem { + this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); + return this.history[this.currentIndex]; + } +} diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index ca25ada12d..df7ba27493 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -61,7 +61,7 @@ import ReplyThread from "../elements/ReplyThread"; import {ContentHelpers} from 'matrix-js-sdk'; import AccessibleButton from '../elements/AccessibleButton'; import {findEditableEvent} from '../../../utils/EventUtils'; -import ComposerHistoryManager from "../../../ComposerHistoryManager"; +import SlateComposerHistoryManager from "../../../SlateComposerHistoryManager"; import TypingStore from "../../../stores/TypingStore"; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -141,7 +141,7 @@ export default class MessageComposerInput extends React.Component { client: MatrixClient; autocomplete: Autocomplete; - historyManager: ComposerHistoryManager; + historyManager: SlateComposerHistoryManager; constructor(props, context) { super(props, context); @@ -331,7 +331,7 @@ export default class MessageComposerInput extends React.Component { componentWillMount() { this.dispatcherRef = dis.register(this.onAction); - this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); + this.historyManager = new SlateComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } componentWillUnmount() { diff --git a/src/components/views/rooms/SlateMessageComposer.js b/src/components/views/rooms/SlateMessageComposer.js new file mode 100644 index 0000000000..d7aa745753 --- /dev/null +++ b/src/components/views/rooms/SlateMessageComposer.js @@ -0,0 +1,489 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 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 React from 'react'; +import PropTypes from 'prop-types'; +import { _t, _td } from '../../../languageHandler'; +import CallHandler from '../../../CallHandler'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import Modal from '../../../Modal'; +import sdk from '../../../index'; +import dis from '../../../dispatcher'; +import RoomViewStore from '../../../stores/RoomViewStore'; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import Stickerpicker from './Stickerpicker'; +import { makeRoomPermalink } from '../../../matrix-to'; +import ContentMessages from '../../../ContentMessages'; +import classNames from 'classnames'; + +import E2EIcon from './E2EIcon'; + +const formatButtonList = [ + _td("bold"), + _td("italic"), + _td("deleted"), + _td("underlined"), + _td("inline-code"), + _td("block-quote"), + _td("bulleted-list"), + _td("numbered-list"), +]; + +function ComposerAvatar(props) { + const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); + return
+ +
; +} + +ComposerAvatar.propTypes = { + me: PropTypes.object.isRequired, +} + +function CallButton(props) { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const onVoiceCallClick = (ev) => { + dis.dispatch({ + action: 'place_call', + type: "voice", + room_id: props.roomId, + }); + }; + + return +} + +CallButton.propTypes = { + roomId: PropTypes.string.isRequired +} + +function VideoCallButton(props) { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const onCallClick = (ev) => { + dis.dispatch({ + action: 'place_call', + type: ev.shiftKey ? "screensharing" : "video", + room_id: props.roomId, + }); + }; + + return ; +} + +VideoCallButton.propTypes = { + roomId: PropTypes.string.isRequired, +}; + +function HangupButton(props) { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const onHangupClick = () => { + const call = CallHandler.getCallForRoom(props.roomId); + if (!call) { + return; + } + dis.dispatch({ + action: 'hangup', + // hangup the call for this room, which may not be the room in props + // (e.g. conferences which will hangup the 1:1 room instead) + room_id: call.roomId, + }); + }; + return ; +} + +HangupButton.propTypes = { + roomId: PropTypes.string.isRequired, +} + +function FormattingButton(props) { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ; +} + +FormattingButton.propTypes = { + showFormatting: PropTypes.bool.isRequired, + onClickHandler: PropTypes.func.isRequired, +} + +class UploadButton extends React.Component { + static propTypes = { + roomId: PropTypes.string.isRequired, + } + constructor(props, context) { + super(props, context); + this.onUploadClick = this.onUploadClick.bind(this); + this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this); + } + + onUploadClick(ev) { + if (MatrixClientPeg.get().isGuest()) { + dis.dispatch({action: 'require_registration'}); + return; + } + this.refs.uploadInput.click(); + } + + onUploadFileInputChange(ev) { + if (ev.target.files.length === 0) return; + + // take a copy so we can safely reset the value of the form control + // (Note it is a FileList: we can't use slice or sesnible iteration). + const tfiles = []; + for (let i = 0; i < ev.target.files.length; ++i) { + tfiles.push(ev.target.files[i]); + } + + ContentMessages.sharedInstance().sendContentListToRoom( + tfiles, this.props.roomId, MatrixClientPeg.get(), + ); + + // This is the onChange handler for a file form control, but we're + // not keeping any state, so reset the value of the form control + // to empty. + // NB. we need to set 'value': the 'files' property is immutable. + ev.target.value = ''; + } + + render() { + const uploadInputStyle = {display: 'none'}; + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + + + ); + } +} + +export default class SlateMessageComposer extends React.Component { + constructor(props, context) { + super(props, context); + this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); + this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this); + this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this); + this.onInputStateChanged = this.onInputStateChanged.bind(this); + this.onEvent = this.onEvent.bind(this); + this._onRoomStateEvents = this._onRoomStateEvents.bind(this); + this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); + this._onTombstoneClick = this._onTombstoneClick.bind(this); + this.renderPlaceholderText = this.renderPlaceholderText.bind(this); + this.renderFormatBar = this.renderFormatBar.bind(this); + + this.state = { + inputState: { + marks: [], + blockType: null, + isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'), + }, + showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'), + isQuoting: Boolean(RoomViewStore.getQuotingEvent()), + tombstone: this._getRoomTombstone(), + canSendMessages: this.props.room.maySendMessage(), + }; + } + + componentDidMount() { + // N.B. using 'event' rather than 'RoomEvents' otherwise the crypto handler + // for 'event' fires *after* 'RoomEvent', and our room won't have yet been + // marked as encrypted. + // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something. + MatrixClientPeg.get().on("event", this.onEvent); + MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); + this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); + this._waitForOwnMember(); + } + + _waitForOwnMember() { + // if we have the member already, do that + const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); + if (me) { + this.setState({me}); + return; + } + // Otherwise, wait for member loading to finish and then update the member for the avatar. + // The members should already be loading, and loadMembersIfNeeded + // will return the promise for the existing operation + this.props.room.loadMembersIfNeeded().then(() => { + const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); + this.setState({me}); + }); + } + + componentWillUnmount() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("event", this.onEvent); + MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents); + } + if (this._roomStoreToken) { + this._roomStoreToken.remove(); + } + } + + onEvent(event) { + if (event.getType() !== 'm.room.encryption') return; + if (event.getRoomId() !== this.props.room.roomId) return; + this.forceUpdate(); + } + + _onRoomStateEvents(ev, state) { + if (ev.getRoomId() !== this.props.room.roomId) return; + + if (ev.getType() === 'm.room.tombstone') { + this.setState({tombstone: this._getRoomTombstone()}); + } + if (ev.getType() === 'm.room.power_levels') { + this.setState({canSendMessages: this.props.room.maySendMessage()}); + } + } + + _getRoomTombstone() { + return this.props.room.currentState.getStateEvents('m.room.tombstone', ''); + } + + _onRoomViewStoreUpdate() { + const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); + if (this.state.isQuoting === isQuoting) return; + this.setState({ isQuoting }); + } + + + onInputStateChanged(inputState) { + // Merge the new input state with old to support partial updates + inputState = Object.assign({}, this.state.inputState, inputState); + this.setState({inputState}); + } + + _onAutocompleteConfirm(range, completion) { + if (this.messageComposerInput) { + this.messageComposerInput.setDisplayedCompletion(range, completion); + } + } + + onFormatButtonClicked(name, event) { + event.preventDefault(); + this.messageComposerInput.onFormatButtonClicked(name, event); + } + + onToggleFormattingClicked() { + SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting); + this.setState({showFormatting: !this.state.showFormatting}); + } + + onToggleMarkdownClicked(e) { + e.preventDefault(); // don't steal focus from the editor! + this.messageComposerInput.enableRichtext(!this.state.inputState.isRichTextEnabled); + } + + _onTombstoneClick(ev) { + ev.preventDefault(); + + const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; + const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId); + let createEventId = null; + if (replacementRoom) { + const createEvent = replacementRoom.currentState.getStateEvents('m.room.create', ''); + if (createEvent && createEvent.getId()) createEventId = createEvent.getId(); + } + + const viaServers = [this.state.tombstone.getSender().split(':').splice(1).join(':')]; + dis.dispatch({ + action: 'view_room', + highlighted: true, + event_id: createEventId, + room_id: replacementRoomId, + auto_join: true, + + // Try to join via the server that sent the event. This converts @something:example.org + // into a server domain by splitting on colons and ignoring the first entry ("@something"). + via_servers: viaServers, + opts: { + // These are passed down to the js-sdk's /join call + viaServers: viaServers, + }, + }); + } + + renderPlaceholderText() { + const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); + if (this.state.isQuoting) { + if (roomIsEncrypted) { + return _t('Send an encrypted reply…'); + } else { + return _t('Send a reply (unencrypted)…'); + } + } else { + if (roomIsEncrypted) { + return _t('Send an encrypted message…'); + } else { + return _t('Send a message (unencrypted)…'); + } + } + } + + renderFormatBar() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const {marks, blockType} = this.state.inputState; + const formatButtons = formatButtonList.map((name) => { + // special-case to match the md serializer and the special-case in MessageComposerInput.js + const markName = name === 'inline-code' ? 'code' : name; + const active = marks.some(mark => mark.type === markName) || blockType === name; + const suffix = active ? '-on' : ''; + const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name); + const className = 'mx_MessageComposer_format_button mx_filterFlipColor'; + return ( + + ); + }) + + return ( +
+
+ { formatButtons } +
+ + +
+
+ ); + } + + render() { + const controls = [ + this.state.me ? : null, + this.props.e2eStatus ? : null, + ]; + + if (!this.state.tombstone && this.state.canSendMessages) { + // This also currently includes the call buttons. Really we should + // check separately for whether we can call, but this is slightly + // complex because of conference calls. + + const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput"); + const showFormattingButton = this.state.inputState.isRichTextEnabled; + const callInProgress = this.props.callState && this.props.callState !== 'ended'; + + controls.push( + this.messageComposerInput = c} + key="controls_input" + room={this.props.room} + placeholder={this.renderPlaceholderText()} + onInputStateChanged={this.onInputStateChanged} + permalinkCreator={this.props.permalinkCreator} />, + showFormattingButton ? : null, + , + , + callInProgress ? : null, + callInProgress ? null : , + callInProgress ? null : , + ); + } else if (this.state.tombstone) { + const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; + + const continuesLink = replacementRoomId ? ( + + {_t("The conversation continues here.")} + + ) : ''; + + controls.push(
+
+ + + {_t("This room has been replaced and is no longer active.")} +
+ { continuesLink } +
+
); + } else { + controls.push( +
+ { _t('You do not have permission to post to this room') } +
, + ); + } + + const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled; + + const wrapperClasses = classNames({ + mx_MessageComposer_wrapper: true, + mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus, + }); + return ( +
+
+
+ { controls } +
+
+ { showFormatBar ? this.renderFormatBar() : null } +
+ ); + } +} + +SlateMessageComposer.propTypes = { + // js-sdk Room object + room: PropTypes.object.isRequired, + + // string representing the current voip call state + callState: PropTypes.string, + + // string representing the current room app drawer state + showApps: PropTypes.bool +}; From 944c56d09bf5c67b1c565aee98ba75980a58f09b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Aug 2019 14:07:03 +0100 Subject: [PATCH 210/413] prevent cider history overlapping with slate composer history --- src/components/views/rooms/SendMessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 7d831dad4a..c8fac0b667 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -235,7 +235,7 @@ export default class SendMessageComposer extends React.Component { const parts = this._restoreStoredEditorState(partCreator) || []; this.model = new EditorModel(parts, partCreator); this.dispatcherRef = dis.register(this.onAction); - this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); + this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_composer_history_'); } get _editorStateKey() { From 6e54bb8e5122ef9eb8aa088eb08d2b080f4092e3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Aug 2019 14:07:31 +0100 Subject: [PATCH 211/413] default is unused here --- src/SendHistoryManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SendHistoryManager.js b/src/SendHistoryManager.js index c838f1246b..794a58ad6f 100644 --- a/src/SendHistoryManager.js +++ b/src/SendHistoryManager.js @@ -23,7 +23,7 @@ export default class SendHistoryManager { lastIndex: number = 0; // used for indexing the storage currentIndex: number = 0; // used for indexing the loaded validated history Array - constructor(roomId: string, prefix: string = 'mx_composer_history_') { + constructor(roomId: string, prefix: string) { this.prefix = prefix + roomId; // TODO: Performance issues? From b395fad8348410a2f957fc37ba263687ad02ec77 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Aug 2019 14:07:43 +0100 Subject: [PATCH 212/413] add feature flag, allowing to revert to old slate editor --- src/components/structures/RoomView.js | 33 +++++++++++++++++++-------- src/settings/Settings.js | 6 +++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 535a1f3df3..5edf19f3ef 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1550,7 +1550,6 @@ module.exports = React.createClass({ render: function() { const RoomHeader = sdk.getComponent('rooms.RoomHeader'); - const MessageComposer = sdk.getComponent('rooms.MessageComposer'); const ForwardMessage = sdk.getComponent("rooms.ForwardMessage"); const AuxPanel = sdk.getComponent("rooms.AuxPanel"); const SearchBar = sdk.getComponent("rooms.SearchBar"); @@ -1778,15 +1777,29 @@ module.exports = React.createClass({ myMembership === 'join' && !this.state.searchResults ); if (canSpeak) { - messageComposer = - ; + if (SettingsStore.isFeatureEnabled("feature_cider_composer")) { + const MessageComposer = sdk.getComponent('rooms.MessageComposer'); + messageComposer = + ; + } else { + const SlateMessageComposer = sdk.getComponent('rooms.SlateMessageComposer'); + messageComposer = + ; + } } // TODO: Why aren't we storing the term/scope/count in this format diff --git a/src/settings/Settings.js b/src/settings/Settings.js index b33ef3f8d7..fd6f2bcdb1 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -114,6 +114,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_cider_composer": { + isFeature: true, + displayName: _td("Use the new, faster, but still experimental composer for writing messages (requires refresh)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "MessageComposerInput.suggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable Emoji suggestions while typing'), From 8c9846efc1bd25d0daeb3576aef5e62aeace8bc3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Aug 2019 14:10:08 +0100 Subject: [PATCH 213/413] update i18n --- src/i18n/strings/en_EN.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 53d8529c65..83a9602a51 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -326,6 +326,7 @@ "Custom user status messages": "Custom user status messages", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", + "Use the new, faster, but still experimental composer for writing messages (requires refresh)": "Use the new, faster, but still experimental composer for writing messages (requires refresh)", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", @@ -747,11 +748,11 @@ " (unsupported)": " (unsupported)", "Join as voice or video.": "Join as voice or video.", "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.", - "Edit message": "Edit message", "Some devices for this user are not trusted": "Some devices for this user are not trusted", "Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted", "All devices for this user are trusted": "All devices for this user are trusted", "All devices in this encrypted room are trusted": "All devices in this encrypted room are trusted", + "Edit message": "Edit message", "This event could not be displayed": "This event could not be displayed", "%(senderName)s sent an image": "%(senderName)s sent an image", "%(senderName)s sent a video": "%(senderName)s sent a video", @@ -804,25 +805,14 @@ "Invited": "Invited", "Filter room members": "Filter room members", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", - "bold": "bold", - "italic": "italic", - "deleted": "deleted", - "underlined": "underlined", - "inline-code": "inline-code", - "block-quote": "block-quote", - "bulleted-list": "bulleted-list", - "numbered-list": "numbered-list", "Voice call": "Voice call", "Video call": "Video call", "Hangup": "Hangup", - "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", "Upload file": "Upload file", "Send an encrypted reply…": "Send an encrypted reply…", "Send a reply (unencrypted)…": "Send a reply (unencrypted)…", "Send an encrypted message…": "Send an encrypted message…", "Send a message (unencrypted)…": "Send a message (unencrypted)…", - "Markdown is disabled": "Markdown is disabled", - "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", "The conversation continues here.": "The conversation continues here.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "You do not have permission to post to this room": "You do not have permission to post to this room", @@ -831,6 +821,7 @@ "Command error": "Command error", "Unable to reply": "Unable to reply", "At this time it is not possible to reply with an emote.": "At this time it is not possible to reply with an emote.", + "Markdown is disabled": "Markdown is disabled", "Markdown is enabled": "Markdown is enabled", "No pinned messages.": "No pinned messages.", "Loading...": "Loading...", @@ -923,7 +914,16 @@ "This Room": "This Room", "All Rooms": "All Rooms", "Search…": "Search…", - "Send message": "Send message", + "bold": "bold", + "italic": "italic", + "deleted": "deleted", + "underlined": "underlined", + "inline-code": "inline-code", + "block-quote": "block-quote", + "bulleted-list": "bulleted-list", + "numbered-list": "numbered-list", + "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", + "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", "Failed to connect to integrations server": "Failed to connect to integrations server", "No integrations server is configured to manage stickers with": "No integrations server is configured to manage stickers with", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", From 9c6953f176e42911f585267fb9d5f4678fb939fe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Aug 2019 14:11:04 +0100 Subject: [PATCH 214/413] lint --- src/settings/Settings.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index fd6f2bcdb1..37a777913b 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -116,7 +116,8 @@ export const SETTINGS = { }, "feature_cider_composer": { isFeature: true, - displayName: _td("Use the new, faster, but still experimental composer for writing messages (requires refresh)"), + displayName: _td("Use the new, faster, but still experimental composer " + + "for writing messages (requires refresh)"), supportedLevels: LEVELS_FEATURE, default: false, }, From 1dd052d9dd79318c1eb53d616c959d55abac755e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Aug 2019 14:38:24 +0100 Subject: [PATCH 215/413] fix test after refactoring --- test/editor/deserialize-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js index c7e0278f52..46deb14ce3 100644 --- a/test/editor/deserialize-test.js +++ b/test/editor/deserialize-test.js @@ -144,7 +144,7 @@ describe('editor/deserialize', function() { const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(3); expect(parts[0]).toStrictEqual({type: "plain", text: "Hi "}); - expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice", userId: "@alice:hs.tld"}); + expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice", resourceId: "@alice:hs.tld"}); expect(parts[2]).toStrictEqual({type: "plain", text: "!"}); }); it('room pill', function() { From 5cebce9bbf557dd5079902d4bcdb5fd34b1caf41 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Aug 2019 14:41:40 +0100 Subject: [PATCH 216/413] fix bug detected by tests --- src/editor/deserialize.js | 8 ++++---- test/editor/deserialize-test.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index d5efef5d1a..d59e4ca123 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -223,14 +223,14 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) { function parsePlainTextMessage(body, partCreator, isQuotedMessage) { const lines = body.split("\n"); const parts = lines.reduce((parts, line, i) => { - const isLast = i === lines.length - 1; - if (!isLast) { - parts.push(partCreator.newline()); - } if (isQuotedMessage) { parts.push(partCreator.plain(QUOTE_LINE_PREFIX)); } parts.push(...parseAtRoomMentions(line, partCreator)); + const isLast = i === lines.length - 1; + if (!isLast) { + parts.push(partCreator.newline()); + } return parts; }, []); return parts; diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js index 46deb14ce3..8a79b4101f 100644 --- a/test/editor/deserialize-test.js +++ b/test/editor/deserialize-test.js @@ -71,10 +71,10 @@ describe('editor/deserialize', function() { describe('text messages', function() { it('test with newlines', function() { const parts = normalize(parseEvent(textMessage("hello\nworld"), createPartCreator())); - expect(parts.length).toBe(3); expect(parts[0]).toStrictEqual({type: "plain", text: "hello"}); expect(parts[1]).toStrictEqual({type: "newline", text: "\n"}); expect(parts[2]).toStrictEqual({type: "plain", text: "world"}); + expect(parts.length).toBe(3); }); it('@room pill', function() { const parts = normalize(parseEvent(textMessage("text message for @room"), createPartCreator())); From e7097d58ecae06d2e9370c03b2654425e01bad47 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 22 Aug 2019 14:44:09 +0100 Subject: [PATCH 217/413] Add IS access token callback This passes a callback to the JS SDK which it can use to get IS access tokens whenever needed for either talking to the IS directly or passing along to the HS. Fixes https://github.com/vector-im/riot-web/issues/10525 --- src/MatrixClientPeg.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 813f0ed87e..94bf6e30d9 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -32,6 +32,7 @@ import Modal from './Modal'; import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; +import IdentityAuthClient from './IdentityAuthClient'; interface MatrixClientCreds { homeserverUrl: string, @@ -219,6 +220,9 @@ class MatrixClientPeg { fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), verificationMethods: [verificationMethods.SAS], unstableClientRelationAggregation: true, + getIdentityAccessToken: () => { + return new IdentityAuthClient().getAccessToken(); + }, }; this.matrixClient = createMatrixClient(opts); From 4a27abb13149feacab54dfb55b49f66c1c3e087c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Aug 2019 15:11:31 +0100 Subject: [PATCH 218/413] fix css lint --- res/css/views/rooms/_BasicMessageComposer.scss | 2 +- res/css/views/rooms/_SendMessageComposer.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index acec4de952..b6035e5859 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -16,7 +16,7 @@ limitations under the License. */ .mx_BasicMessageComposer { - .mx_BasicMessageComposer_inputEmpty > :first-child:before { + .mx_BasicMessageComposer_inputEmpty > :first-child::before { content: var(--placeholder); opacity: 0.333; width: 0; diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index 3304003d84..d20f7107b3 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -20,7 +20,6 @@ limitations under the License. flex-direction: column; font-size: 14px; justify-content: center; - display: flex; margin-right: 6px; // don't grow wider than available space min-width: 0; From cd6a980c7ed8cab5126668642038deac390186f9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Aug 2019 16:57:51 +0100 Subject: [PATCH 219/413] Only Destroy the expected persistent widget, not *ANY* Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/AppTile.js | 7 +++---- src/stores/ActiveWidgetStore.js | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index a9239303b1..55ccfc8168 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -154,10 +154,9 @@ export default class AppTile extends React.Component { // Widget action listeners dis.unregister(this.dispatcherRef); - const canPersist = this.props.whitelistCapabilities.includes('m.always_on_screen'); // if it's not remaining on screen, get rid of the PersistedElement container - if (canPersist && !ActiveWidgetStore.getWidgetPersistence(this.props.id)) { - ActiveWidgetStore.destroyPersistentWidget(); + if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) { + ActiveWidgetStore.destroyPersistentWidget(this.props.id); const PersistedElement = sdk.getComponent("elements.PersistedElement"); PersistedElement.destroyElement(this._persistKey); } @@ -429,7 +428,7 @@ export default class AppTile extends React.Component { this.setState({hasPermissionToLoad: false}); // Force the widget to be non-persistent - ActiveWidgetStore.destroyPersistentWidget(); + ActiveWidgetStore.destroyPersistentWidget(this.props.id); const PersistedElement = sdk.getComponent("elements.PersistedElement"); PersistedElement.destroyElement(this._persistKey); } diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js index f0a9f718a2..82a7f7932c 100644 --- a/src/stores/ActiveWidgetStore.js +++ b/src/stores/ActiveWidgetStore.js @@ -67,11 +67,12 @@ class ActiveWidgetStore extends EventEmitter { if (ev.getType() !== 'im.vector.modular.widgets') return; if (ev.getStateKey() === this._persistentWidgetId) { - this.destroyPersistentWidget(); + this.destroyPersistentWidget(this._persistentWidgetId); } } - destroyPersistentWidget() { + destroyPersistentWidget(id) { + if (id !== this._persistentWidgetId) return; const toDeleteId = this._persistentWidgetId; this.setWidgetPersistence(toDeleteId, false); From b5daba90261df41b8d39d98966a48c0a113b416e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Aug 2019 18:17:08 +0100 Subject: [PATCH 220/413] Iterate over all instances of variable/tag for _t substitutions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/languageHandler.js | 78 +++++++++++++++----------- test/i18n-test/languageHandler-test.js | 11 ++++ 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 474cd2b3cd..9e354cee9e 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -179,12 +179,12 @@ export function replaceByRegexes(text, mapping) { for (const regexpString in mapping) { // TODO: Cache regexps - const regexp = new RegExp(regexpString); + const regexp = new RegExp(regexpString, "g"); // Loop over what output we have so far and perform replacements // We look for matches: if we find one, we get three parts: everything before the match, the replaced part, // and everything after the match. Insert all three into the output. We need to do this because we can insert objects. - // Otherwise there would be no need for the splitting and we could do simple replcement. + // Otherwise there would be no need for the splitting and we could do simple replacement. let matchFoundSomewhere = false; // If we don't find a match anywhere we want to log it for (const outputIndex in output) { const inputText = output[outputIndex]; @@ -192,44 +192,58 @@ export function replaceByRegexes(text, mapping) { continue; } - const match = inputText.match(regexp); - if (!match) { - continue; - } + // process every match in the string + // starting with the first + let match = regexp.exec(inputText); + + if (!match) continue; matchFoundSomewhere = true; - const capturedGroups = match.slice(2); - - // The textual part before the match + // The textual part before the first match const head = inputText.substr(0, match.index); - // The textual part after the match - const tail = inputText.substr(match.index + match[0].length); + const parts = []; + // keep track of prevMatch + let prevMatch; + while (match) { + // store prevMatch + prevMatch = match; + const capturedGroups = match.slice(2); - let replaced; - // If substitution is a function, call it - if (mapping[regexpString] instanceof Function) { - replaced = mapping[regexpString].apply(null, capturedGroups); - } else { - replaced = mapping[regexpString]; + let replaced; + // If substitution is a function, call it + if (mapping[regexpString] instanceof Function) { + replaced = mapping[regexpString].apply(null, capturedGroups); + } else { + replaced = mapping[regexpString]; + } + + if (typeof replaced === 'object') { + shouldWrapInSpan = true; + } + + // Here we also need to check that it actually is a string before comparing against one + // The head and tail are always strings + if (typeof replaced !== 'string' || replaced !== '') { + parts.push(replaced); + } + + // try the next match + match = regexp.exec(inputText); + + // add the text between prevMatch and this one + // or the end of the string if prevMatch is the last match + if (match) { + const startIndex = prevMatch.index + prevMatch[0].length; + parts.push(inputText.substr(startIndex, match.index - startIndex)); + } else { + parts.push(inputText.substr(prevMatch.index + prevMatch[0].length)); + } } - if (typeof replaced === 'object') { - shouldWrapInSpan = true; - } - - output.splice(outputIndex, 1); // Remove old element - // Insert in reverse order as splice does insert-before and this way we get the final order correct - if (tail !== '') { - output.splice(outputIndex, 0, tail); - } - - // Here we also need to check that it actually is a string before comparing against one - // The head and tail are always strings - if (typeof replaced !== 'string' || replaced !== '') { - output.splice(outputIndex, 0, replaced); - } + // remove the old element at the same time + output.splice(outputIndex, 1, ...parts); if (head !== '') { // Don't push empty nodes, they are of no use output.splice(outputIndex, 0, head); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index ce9f8e1684..07e3f2cb8b 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -70,4 +70,15 @@ describe('languageHandler', function() { const text = '%(var1)s %(var2)s'; expect(languageHandler._t(text, { var2: 'val2', var1: 'val1' })).toBe('val1 val2'); }); + + it('multiple replacements of the same variable', function() { + const text = '%(var1)s %(var1)s'; + expect(languageHandler._t(text, { var1: 'val1' })).toBe('val1 val1'); + }); + + it('multiple replacements of the same tag', function() { + const text = 'Click here to join the discussion! or here'; + expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` })) + .toBe('xClick herex to join the discussion! xor herex'); + }); }); From 310457059b32b0503c8575d8a6093599000e4ade Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Aug 2019 18:31:02 +0100 Subject: [PATCH 221/413] [i18n] only append tail if it is actually needed Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/languageHandler.js | 12 ++++++++++-- test/i18n-test/languageHandler-test.js | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 9e354cee9e..e5656e5f69 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -177,6 +177,10 @@ export function replaceByRegexes(text, mapping) { // If we insert any components we need to wrap the output in a span. React doesn't like just an array of components. let shouldWrapInSpan = false; + if (text === "You are now ignoring %(userId)s") { + debugger; + } + for (const regexpString in mapping) { // TODO: Cache regexps const regexp = new RegExp(regexpString, "g"); @@ -233,11 +237,15 @@ export function replaceByRegexes(text, mapping) { // add the text between prevMatch and this one // or the end of the string if prevMatch is the last match + let tail; if (match) { const startIndex = prevMatch.index + prevMatch[0].length; - parts.push(inputText.substr(startIndex, match.index - startIndex)); + tail = inputText.substr(startIndex, match.index - startIndex); } else { - parts.push(inputText.substr(prevMatch.index + prevMatch[0].length)); + tail = inputText.substr(prevMatch.index + prevMatch[0].length); + } + if (tail) { + parts.push(tail); } } diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 07e3f2cb8b..0d96bc15ab 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -73,12 +73,12 @@ describe('languageHandler', function() { it('multiple replacements of the same variable', function() { const text = '%(var1)s %(var1)s'; - expect(languageHandler._t(text, { var1: 'val1' })).toBe('val1 val1'); + expect(languageHandler.substitute(text, { var1: 'val1' })).toBe('val1 val1'); }); it('multiple replacements of the same tag', function() { const text = 'Click here to join the discussion! or here'; - expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` })) + expect(languageHandler.substitute(text, {}, { 'a': (sub) => `x${sub}x` })) .toBe('xClick herex to join the discussion! xor herex'); }); }); From 7d511fbbc5d0513f8575464cd58d4e0c65bea9f4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Aug 2019 18:34:26 +0100 Subject: [PATCH 222/413] remove leftover debugger =) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/languageHandler.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index e5656e5f69..179bb2d1d0 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -177,10 +177,6 @@ export function replaceByRegexes(text, mapping) { // If we insert any components we need to wrap the output in a span. React doesn't like just an array of components. let shouldWrapInSpan = false; - if (text === "You are now ignoring %(userId)s") { - debugger; - } - for (const regexpString in mapping) { // TODO: Cache regexps const regexp = new RegExp(regexpString, "g"); From a35735da45322570d3cb4edd66b54e50c59beda8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Aug 2019 14:49:20 -0600 Subject: [PATCH 223/413] Support homeserver-configured integration managers Fixes https://github.com/vector-im/riot-web/issues/4913 Requires https://github.com/matrix-org/matrix-js-sdk/pull/1024 Implements part of [MSC1957](https://github.com/matrix-org/matrix-doc/pull/1957) --- .../IntegrationManagerInstance.js | 2 + src/integrations/IntegrationManagers.js | 72 ++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index 6744d82e75..d36fa73d48 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -23,11 +23,13 @@ import url from 'url'; export const KIND_ACCOUNT = "account"; export const KIND_CONFIG = "config"; +export const KIND_HOMESERVER = "homeserver"; export class IntegrationManagerInstance { apiUrl: string; uiUrl: string; kind: string; + id: string; // only applicable in some cases constructor(kind: string, apiUrl: string, uiUrl: string) { this.kind = kind; diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 0e19c7add0..52486bd30e 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -17,10 +17,19 @@ limitations under the License. import SdkConfig from '../SdkConfig'; import sdk from "../index"; import Modal from '../Modal'; -import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG} from "./IntegrationManagerInstance"; +import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG, KIND_HOMESERVER} from "./IntegrationManagerInstance"; import type {MatrixClient, MatrixEvent} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; +import {AutoDiscovery} from "../../../matrix-js-sdk"; + +const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours +const KIND_PREFERENCE = [ + // Ordered: first is most preferred, last is least preferred. + KIND_ACCOUNT, + KIND_HOMESERVER, + KIND_CONFIG, +]; export class IntegrationManagers { static _instance; @@ -34,6 +43,8 @@ export class IntegrationManagers { _managers: IntegrationManagerInstance[] = []; _client: MatrixClient; + _wellknownRefreshTimerId: number = null; + _primaryManager: IntegrationManagerInstance; constructor() { this._compileManagers(); @@ -44,16 +55,19 @@ export class IntegrationManagers { this._client = MatrixClientPeg.get(); this._client.on("accountData", this._onAccountData.bind(this)); this._compileManagers(); + setInterval(() => this._setupHomeserverManagers(), HS_MANAGERS_REFRESH_INTERVAL); } stopWatching(): void { if (!this._client) return; this._client.removeListener("accountData", this._onAccountData.bind(this)); + if (this._wellknownRefreshTimerId !== null) clearInterval(this._wellknownRefreshTimerId); } _compileManagers() { this._managers = []; this._setupConfiguredManager(); + this._setupHomeserverManagers(); this._setupAccountManagers(); } @@ -63,6 +77,42 @@ export class IntegrationManagers { if (apiUrl && uiUrl) { this._managers.push(new IntegrationManagerInstance(KIND_CONFIG, apiUrl, uiUrl)); + this._primaryManager = null; // reset primary + } + } + + async _setupHomeserverManagers() { + try { + console.log("Updating homeserver-configured integration managers..."); + const homeserverDomain = MatrixClientPeg.getHomeserverName(); + const discoveryResponse = await AutoDiscovery.getRawClientConfig(homeserverDomain); + if (discoveryResponse && discoveryResponse['m.integrations']) { + let managers = discoveryResponse['m.integrations']['managers']; + if (!Array.isArray(managers)) managers = []; // make it an array so we can wipe the HS managers + + console.log(`Homeserver has ${managers.length} integration managers`); + + // Clear out any known managers for the homeserver + // TODO: Log out of the scalar clients + this._managers = this._managers.filter(m => m.kind !== KIND_HOMESERVER); + + // Now add all the managers the homeserver wants us to have + for (const hsManager of managers) { + if (!hsManager["api_url"]) continue; + this._managers.push(new IntegrationManagerInstance( + KIND_HOMESERVER, + hsManager["api_url"], + hsManager["ui_url"], // optional + )); + } + + this._primaryManager = null; // reset primary + } else { + console.log("Homeserver has no integration managers"); + } + } catch (e) { + console.error(e); + // Errors during discovery are non-fatal } } @@ -77,8 +127,11 @@ export class IntegrationManagers { const apiUrl = data['api_url']; if (!apiUrl || !uiUrl) return; - this._managers.push(new IntegrationManagerInstance(KIND_ACCOUNT, apiUrl, uiUrl)); + const manager = new IntegrationManagerInstance(KIND_ACCOUNT, apiUrl, uiUrl); + manager.id = w['id'] || w['state_key'] || ''; + this._managers.push(manager); }); + this._primaryManager = null; // reset primary } _onAccountData(ev: MatrixEvent): void { @@ -93,7 +146,20 @@ export class IntegrationManagers { getPrimaryManager(): IntegrationManagerInstance { if (this.hasManager()) { - return this._managers[this._managers.length - 1]; + if (this._primaryManager) return this._primaryManager; + + for (const kind of KIND_PREFERENCE) { + const managers = this._managers.filter(m => m.kind === kind); + if (!managers || !managers.length) continue; + + if (kind === KIND_ACCOUNT) { + // Order by state_keys (IDs) + managers.sort((a, b) => a.id.localeCompare(b.id)); + } + + this._primaryManager = managers[0]; + return this._primaryManager; + } } else { return null; } From 8493887cebdf71e21124ffd006ec0262c54f1b25 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Aug 2019 14:57:09 -0600 Subject: [PATCH 224/413] Import the right js-sdk --- src/integrations/IntegrationManagers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 52486bd30e..f663698da9 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -21,7 +21,7 @@ import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG, KIND_HOMESERVER} import type {MatrixClient, MatrixEvent} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; -import {AutoDiscovery} from "../../../matrix-js-sdk"; +import {AutoDiscovery} from "matrix-js-sdk"; const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours const KIND_PREFERENCE = [ From 470295ad14cbdf44638ba3f98201b39e160f28ab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Aug 2019 15:17:59 -0600 Subject: [PATCH 225/413] Expose a getOrderedManagers() function for use elsewhere --- src/integrations/IntegrationManagers.js | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index f663698da9..98f605353c 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -144,22 +144,28 @@ export class IntegrationManagers { return this._managers.length > 0; } + getOrderedManagers(): IntegrationManagerInstance[] { + const ordered = []; + for (const kind of KIND_PREFERENCE) { + const managers = this._managers.filter(m => m.kind === kind); + if (!managers || !managers.length) continue; + + if (kind === KIND_ACCOUNT) { + // Order by state_keys (IDs) + managers.sort((a, b) => a.id.localeCompare(b.id)); + } + + ordered.push(...managers); + } + return ordered; + } + getPrimaryManager(): IntegrationManagerInstance { if (this.hasManager()) { if (this._primaryManager) return this._primaryManager; - for (const kind of KIND_PREFERENCE) { - const managers = this._managers.filter(m => m.kind === kind); - if (!managers || !managers.length) continue; - - if (kind === KIND_ACCOUNT) { - // Order by state_keys (IDs) - managers.sort((a, b) => a.id.localeCompare(b.id)); - } - - this._primaryManager = managers[0]; - return this._primaryManager; - } + this._primaryManager = this.getOrderedManagers()[0]; + return this._primaryManager; } else { return null; } From f505aa0c8330557256693d9c01166f32347c74b3 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 23 Aug 2019 10:12:46 +0300 Subject: [PATCH 226/413] Ensure logging tweak doesn't fail on undefined Run the replace on the log line string instead of the separate parts since we can ensure the line is a string. Signed-off-by: Jason Robinson --- src/rageshake/rageshake.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index 87c98d105a..1acce0600c 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -74,17 +74,13 @@ class ConsoleLogger { // Convert objects and errors to helpful things args = args.map((arg) => { - let msg = ''; if (arg instanceof Error) { - msg = arg.message + (arg.stack ? `\n${arg.stack}` : ''); + return arg.message + (arg.stack ? `\n${arg.stack}` : ''); } else if (typeof(arg) === 'object') { - msg = JSON.stringify(arg); + return JSON.stringify(arg); } else { - msg = arg; + return arg; } - // Do some cleanup - msg = msg.replace(/token=[a-zA-Z0-9-]+/gm, 'token=xxxxx'); - return msg; }); // Some browsers support string formatting which we're not doing here @@ -92,7 +88,9 @@ class ConsoleLogger { // run. // Example line: // 2017-01-18T11:23:53.214Z W Failed to set badge count - const line = `${ts} ${level} ${args.join(' ')}\n`; + let line = `${ts} ${level} ${args.join(' ')}\n`; + // Do some cleanup + line = line.replace(/token=[a-zA-Z0-9-]+/gm, 'token=xxxxx'); // Using + really is the quickest way in JS // http://jsperf.com/concat-vs-plus-vs-join this.logs += line; From 84e3d339ac0a67035c0b0330f7470bbec136c52a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 23 Aug 2019 11:17:51 +0100 Subject: [PATCH 227/413] Change to provider object --- src/MatrixClientPeg.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 94bf6e30d9..27c4f40669 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -220,9 +220,7 @@ class MatrixClientPeg { fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), verificationMethods: [verificationMethods.SAS], unstableClientRelationAggregation: true, - getIdentityAccessToken: () => { - return new IdentityAuthClient().getAccessToken(); - }, + identityServer: new IdentityAuthClient(), }; this.matrixClient = createMatrixClient(opts); From b3cda4b19a2185a40cba2a505cea24b874adcbb5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Aug 2019 09:12:40 -0600 Subject: [PATCH 228/413] Support multiple integration managers behind a labs flag Fixes https://github.com/vector-im/riot-web/issues/10622 Implements [MSC1957](https://github.com/matrix-org/matrix-doc/pull/1957) Design is not final. --- res/css/_components.scss | 1 + .../_TabbedIntegrationManagerDialog.scss | 67 +++++++ src/FromWidgetPostMessageApi.js | 19 +- src/ScalarMessaging.js | 9 +- .../dialogs/TabbedIntegrationManagerDialog.js | 172 ++++++++++++++++++ src/components/views/elements/AppTile.js | 19 +- .../views/elements/ManageIntegsButton.js | 7 +- src/components/views/rooms/AppsDrawer.js | 7 +- src/components/views/rooms/Stickerpicker.js | 19 +- .../views/settings/IntegrationsManager.js | 2 +- src/i18n/strings/en_EN.json | 1 + src/integrations/IntegrationManagers.js | 10 +- src/settings/Settings.js | 6 + 13 files changed, 318 insertions(+), 21 deletions(-) create mode 100644 res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss create mode 100644 src/components/views/dialogs/TabbedIntegrationManagerDialog.js diff --git a/res/css/_components.scss b/res/css/_components.scss index d19d07132c..fb6058df00 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -71,6 +71,7 @@ @import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_SlashCommandHelpDialog.scss"; +@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @import "./views/dialogs/_TermsDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss"; diff --git a/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss b/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss new file mode 100644 index 0000000000..bb55284391 --- /dev/null +++ b/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss @@ -0,0 +1,67 @@ +/* +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. +*/ + +.mx_TabbedIntegrationManagerDialog .mx_Dialog { + width: 60%; + height: 70%; + overflow: hidden; + padding: 0; + max-width: initial; + max-height: initial; + position: relative; +} + +.mx_TabbedIntegrationManagerDialog_container { + // Full size of the dialog, whatever it is + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + + .mx_TabbedIntegrationManagerDialog_currentManager { + width: 100%; + height: 100%; + border-top: 1px solid $accent-color; + + iframe { + background-color: #fff; + border: 0; + width: 100%; + height: 100%; + } + } +} + +.mx_TabbedIntegrationManagerDialog_tab { + display: inline-block; + border: 1px solid $accent-color; + border-bottom: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + //background-color: $accent-color-50pct; + padding: 10px 8px; + margin-right: 5px; +} + +.mx_TabbedIntegrationManagerDialog_tab:first-child { + //margin-left: 8px; +} + +.mx_TabbedIntegrationManagerDialog_currentTab { + background-color: $accent-color; + color: $accent-fg-color; +} \ No newline at end of file diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index b2bd579b74..8915c1412f 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -23,6 +23,7 @@ import ActiveWidgetStore from './stores/ActiveWidgetStore'; import MatrixClientPeg from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; +import SettingsStore from "./settings/SettingsStore"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -194,11 +195,19 @@ export default class FromWidgetPostMessageApi { const integId = (data && data.integId) ? data.integId : null; // TODO: Open the right integration manager for the widget - IntegrationManagers.sharedInstance().getPrimaryManager().open( - MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), - `type_${integType}`, - integId, - ); + if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { + IntegrationManagers.sharedInstance().openAll( + MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), + `type_${integType}`, + integId, + ); + } else { + IntegrationManagers.sharedInstance().getPrimaryManager().open( + MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), + `type_${integType}`, + integId, + ); + } } else if (action === 'set_always_on_screen') { // This is a new message: there is no reason to support the deprecated widgetData here const data = event.data.data; diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 0d61755519..e8a51dcf4c 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -548,8 +548,8 @@ const onMessage = function(event) { // (See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) let configUrl; try { - // TODO: Support multiple integration managers - configUrl = new URL(IntegrationManagers.sharedInstance().getPrimaryManager().uiUrl); + if (!openManagerUrl) openManagerUrl = IntegrationManagers.sharedInstance().getPrimaryManager().uiUrl; + configUrl = new URL(openManagerUrl); } catch (e) { // No integrations UI URL, ignore silently. return; @@ -657,6 +657,7 @@ const onMessage = function(event) { }; let listenerCount = 0; +let openManagerUrl = null; module.exports = { startListening: function() { if (listenerCount === 0) { @@ -679,4 +680,8 @@ module.exports = { console.error(e); } }, + + setOpenManagerUrl: function(url) { + openManagerUrl = url; + } }; diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js new file mode 100644 index 0000000000..5e3b4688a4 --- /dev/null +++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js @@ -0,0 +1,172 @@ +/* +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; +import {Room} from "matrix-js-sdk"; +import sdk from '../../../index'; +import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms"; +import classNames from 'classnames'; +import ScalarMessaging from "../../../ScalarMessaging"; + +export default class TabbedIntegrationManagerDialog extends React.Component { + static propTypes = { + /** + * Called with: + * * success {bool} True if the user accepted any douments, false if cancelled + * * agreedUrls {string[]} List of agreed URLs + */ + onFinished: PropTypes.func.isRequired, + + /** + * Optional room where the integration manager should be open to + */ + room: PropTypes.instanceOf(Room), + + /** + * Optional screen to open on the integration manager + */ + screen: PropTypes.string, + + /** + * Optional integration ID to open in the integration manager + */ + integrationId: PropTypes.string, + }; + + constructor(props) { + super(props); + + this.state = { + managers: IntegrationManagers.sharedInstance().getOrderedManagers(), + busy: true, + currentIndex: 0, + currentConnected: false, + currentLoading: true, + currentScalarClient: null, + }; + } + + componentDidMount(): void { + this.openManager(0, true); + } + + openManager = async (i: number, force = false) => { + if (i === this.state.currentIndex && !force) return; + + const manager = this.state.managers[i]; + const client = manager.getScalarClient(); + this.setState({ + busy: true, + currentIndex: i, + currentLoading: true, + currentConnected: false, + currentScalarClient: client, + }); + + ScalarMessaging.setOpenManagerUrl(manager.uiUrl); + + client.setTermsInteractionCallback((policyInfo, agreedUrls) => { + // To avoid visual glitching of two modals stacking briefly, we customise the + // terms dialog sizing when it will appear for the integrations manager so that + // it gets the same basic size as the IM's own modal. + return dialogTermsInteractionCallback( + policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager', + ); + }); + + try { + await client.connect(); + if (!client.hasCredentials()) { + this.setState({ + busy: false, + currentLoading: false, + currentConnected: false, + }); + } else { + this.setState({ + busy: false, + currentLoading: false, + currentConnected: true, + }); + } + } catch (e) { + if (e instanceof TermsNotSignedError) { + return; + } + + console.error(e); + this.setState({ + busy: false, + currentLoading: false, + currentConnected: false, + }); + } + }; + + _renderTabs() { + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + return this.state.managers.map((m, i) => { + const classes = classNames({ + 'mx_TabbedIntegrationManagerDialog_tab': true, + 'mx_TabbedIntegrationManagerDialog_currentTab': this.state.currentIndex === i, + }); + return ( + this.openManager(i)} + key={`tab_${i}`} + disabled={this.state.busy} + > + {m.name} + + ); + }) + } + + _renderTab() { + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + let uiUrl = null; + if (this.state.currentScalarClient) { + uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom( + this.props.room, + this.props.screen, + this.props.integrationId, + ); + } + return {/* no-op */}} + />; + } + + render() { + return ( +
+
+ {this._renderTabs()} +
+
+ {this._renderTab()} +
+
+ ) + } +} diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 9e3570a608..ef6c45e0a3 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -34,6 +34,7 @@ import dis from '../../../dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; +import SettingsStore from "../../../settings/SettingsStore"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -264,11 +265,19 @@ export default class AppTile extends React.Component { this.props.onEditClick(); } else { // TODO: Open the right manager for the widget - IntegrationManagers.sharedInstance().getPrimaryManager().open( - this.props.room, - 'type_' + this.props.type, - this.props.id, - ); + if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { + IntegrationManagers.sharedInstance().openAll( + this.props.room, + 'type_' + this.props.type, + this.props.id, + ); + } else { + IntegrationManagers.sharedInstance().getPrimaryManager().open( + this.props.room, + 'type_' + this.props.type, + this.props.id, + ); + } } } diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index ca7391329f..3503d1713b 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -20,6 +20,7 @@ import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; +import SettingsStore from "../../../settings/SettingsStore"; export default class ManageIntegsButton extends React.Component { constructor(props) { @@ -33,7 +34,11 @@ export default class ManageIntegsButton extends React.Component { if (!managers.hasManager()) { managers.openNoManagerDialog(); } else { - managers.getPrimaryManager().open(this.props.room); + if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { + managers.openAll(this.props.room); + } else { + managers.getPrimaryManager().open(this.props.room); + } } }; diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 4d2c1e0380..19a5a6c468 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -30,6 +30,7 @@ import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetEchoStore from "../../../stores/WidgetEchoStore"; import AccessibleButton from '../elements/AccessibleButton'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; +import SettingsStore from "../../../settings/SettingsStore"; // The maximum number of widgets that can be added in a room const MAX_WIDGETS = 2; @@ -128,7 +129,11 @@ module.exports = React.createClass({ }, _launchManageIntegrations: function() { - IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ'); + if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { + IntegrationManagers.sharedInstance().openAll(); + } else { + IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ'); + } }, onClickAddWidget: function(e) { diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 2d3508c404..abecb1781d 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -24,6 +24,7 @@ import WidgetUtils from '../../../utils/WidgetUtils'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import PersistedElement from "../elements/PersistedElement"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; +import SettingsStore from "../../../settings/SettingsStore"; const widgetType = 'm.stickerpicker'; @@ -349,11 +350,19 @@ export default class Stickerpicker extends React.Component { */ _launchManageIntegrations() { // TODO: Open the right integration manager for the widget - IntegrationManagers.sharedInstance().getPrimaryManager().open( - this.props.room, - `type_${widgetType}`, - this.state.widgetId, - ); + if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { + IntegrationManagers.sharedInstance().openAll( + this.props.room, + `type_${widgetType}`, + this.state.widgetId, + ); + } else { + IntegrationManagers.sharedInstance().getPrimaryManager().open( + this.props.room, + `type_${widgetType}`, + this.state.widgetId, + ); + } } render() { diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationsManager.js index 149d66eef6..d463b043d5 100644 --- a/src/components/views/settings/IntegrationsManager.js +++ b/src/components/views/settings/IntegrationsManager.js @@ -43,7 +43,7 @@ export default class IntegrationsManager extends React.Component { configured: true, connected: true, loading: false, - } + }; componentDidMount() { this.dispatcherRef = dis.register(this.onAction); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 83a9602a51..53429fe735 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -327,6 +327,7 @@ "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", "Use the new, faster, but still experimental composer for writing messages (requires refresh)": "Use the new, faster, but still experimental composer for writing messages (requires refresh)", + "Multiple integration managers": "Multiple integration managers", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 98f605353c..a0fbff56fb 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -18,7 +18,7 @@ import SdkConfig from '../SdkConfig'; import sdk from "../index"; import Modal from '../Modal'; import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG, KIND_HOMESERVER} from "./IntegrationManagerInstance"; -import type {MatrixClient, MatrixEvent} from "matrix-js-sdk"; +import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; import {AutoDiscovery} from "matrix-js-sdk"; @@ -180,6 +180,14 @@ export class IntegrationManagers { ); } + openAll(room: Room = null, screen: string = null, integrationId: string = null): void { + const TabbedIntegrationManagerDialog = sdk.getComponent("views.dialogs.TabbedIntegrationManagerDialog"); + Modal.createTrackedDialog( + 'Tabbed Integration Manager', '', TabbedIntegrationManagerDialog, + {room, screen, integrationId}, 'mx_TabbedIntegrationManagerDialog', + ); + } + async overwriteManagerOnAccount(manager: IntegrationManagerInstance) { // TODO: TravisR - We should be logging out of scalar clients. await WidgetUtils.removeIntegrationManagerWidgets(); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 37a777913b..70abf406b8 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -121,6 +121,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_many_integration_managers": { + isFeature: true, + displayName: _td("Multiple integration managers"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "MessageComposerInput.suggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable Emoji suggestions while typing'), From 160396ca9ef132173a74ea4aedc2fc98175470eb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Aug 2019 09:16:44 -0600 Subject: [PATCH 229/413] Appease the linter --- src/ScalarMessaging.js | 2 +- .../views/dialogs/TabbedIntegrationManagerDialog.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index e8a51dcf4c..910a6c4f13 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -683,5 +683,5 @@ module.exports = { setOpenManagerUrl: function(url) { openManagerUrl = url; - } + }, }; diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js index 5e3b4688a4..5ef7aef9ab 100644 --- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js +++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js @@ -135,7 +135,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component { {m.name}
); - }) + }); } _renderTab() { @@ -167,6 +167,6 @@ export default class TabbedIntegrationManagerDialog extends React.Component { {this._renderTab()}
- ) + ); } } From 266e3af475c92b8977c125bf9a4652661f774726 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Aug 2019 09:20:28 -0600 Subject: [PATCH 230/413] Appease the scss linter --- .../_TabbedIntegrationManagerDialog.scss | 71 +++++++++---------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss b/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss index bb55284391..0ab59c44a7 100644 --- a/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss +++ b/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss @@ -15,53 +15,48 @@ limitations under the License. */ .mx_TabbedIntegrationManagerDialog .mx_Dialog { - width: 60%; - height: 70%; - overflow: hidden; - padding: 0; - max-width: initial; - max-height: initial; - position: relative; + width: 60%; + height: 70%; + overflow: hidden; + padding: 0; + max-width: initial; + max-height: initial; + position: relative; } .mx_TabbedIntegrationManagerDialog_container { - // Full size of the dialog, whatever it is - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; + // Full size of the dialog, whatever it is + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; - .mx_TabbedIntegrationManagerDialog_currentManager { - width: 100%; - height: 100%; - border-top: 1px solid $accent-color; + .mx_TabbedIntegrationManagerDialog_currentManager { + width: 100%; + height: 100%; + border-top: 1px solid $accent-color; - iframe { - background-color: #fff; - border: 0; - width: 100%; - height: 100%; + iframe { + background-color: #fff; + border: 0; + width: 100%; + height: 100%; + } } - } } .mx_TabbedIntegrationManagerDialog_tab { - display: inline-block; - border: 1px solid $accent-color; - border-bottom: 0; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - //background-color: $accent-color-50pct; - padding: 10px 8px; - margin-right: 5px; -} - -.mx_TabbedIntegrationManagerDialog_tab:first-child { - //margin-left: 8px; + display: inline-block; + border: 1px solid $accent-color; + border-bottom: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + padding: 10px 8px; + margin-right: 5px; } .mx_TabbedIntegrationManagerDialog_currentTab { - background-color: $accent-color; - color: $accent-fg-color; -} \ No newline at end of file + background-color: $accent-color; + color: $accent-fg-color; +} From 72ec6c7062f19a5c261ce14ea88f5d2c5970b54b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 23 Aug 2019 18:43:55 +0100 Subject: [PATCH 231/413] Reveal custom IS field only when required This hides the identity server at first from the custom server auth flows. For the flows that may need an IS if the HS requires it (registration, password reset), we then check with the HS before proceeding further and reveal the IS field if it is in fact needed. Fixes https://github.com/vector-im/riot-web/issues/10553 --- res/css/views/auth/_ServerConfig.scss | 28 ++--- .../structures/auth/ForgotPassword.js | 2 + .../structures/auth/Registration.js | 1 + src/components/views/auth/ServerConfig.js | 109 ++++++++++++++---- src/i18n/strings/en_EN.json | 3 +- 5 files changed, 100 insertions(+), 43 deletions(-) diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss index a31feb75d7..a7e0057ab3 100644 --- a/res/css/views/auth/_ServerConfig.scss +++ b/res/css/views/auth/_ServerConfig.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket 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. @@ -14,23 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ServerConfig_fields { - display: flex; - margin: 1em 0; -} - -.mx_ServerConfig_fields .mx_Field { - margin: 0 5px; -} - -.mx_ServerConfig_fields .mx_Field:first-child { - margin-left: 0; -} - -.mx_ServerConfig_fields .mx_Field:last-child { - margin-right: 0; -} - .mx_ServerConfig_help:link { opacity: 0.8; } @@ -39,3 +23,13 @@ limitations under the License. display: block; color: $warning-color; } + +.mx_ServerConfig_identityServer { + transform: scaleY(0); + transform-origin: top; + transition: transform 0.25s; + + &.mx_ServerConfig_identityServer_shown { + transform: scaleY(1); + } +} diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 6d80f66d64..11c0ff8295 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017, 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. @@ -208,6 +209,7 @@ module.exports = React.createClass({ serverConfig={this.props.serverConfig} onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={0} + showIdentityServerIfRequiredByHomeserver={true} onAfterSubmit={this.onServerDetailsNextPhaseClick} submitText={_t("Next")} submitClass="mx_Login_submit" diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 63c5b267cf..2fd028ea1d 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -499,6 +499,7 @@ module.exports = React.createClass({ serverConfig={this.props.serverConfig} onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} + showIdentityServerIfRequiredByHomeserver={true} {...serverDetailsProps} />; break; diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index 467ba307d0..7d7a99a79c 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd 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. @@ -23,6 +24,8 @@ import { _t } from '../../../languageHandler'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import SdkConfig from "../../../SdkConfig"; +import { createClient } from 'matrix-js-sdk/lib/matrix'; +import classNames from 'classnames'; /* * A pure UI component which displays the HS and IS to use. @@ -46,6 +49,10 @@ export default class ServerConfig extends React.PureComponent { // Optional class for the submit button. Only applies if the submit button // is to be rendered. submitClass: PropTypes.string, + + // Whether the flow this component is embedded in requires an identity + // server when the homeserver says it will need one. + showIdentityServerIfRequiredByHomeserver: PropTypes.bool, }; static defaultProps = { @@ -61,6 +68,7 @@ export default class ServerConfig extends React.PureComponent { errorText: "", hsUrl: props.serverConfig.hsUrl, isUrl: props.serverConfig.isUrl, + showIdentityServer: false, }; } @@ -75,7 +83,29 @@ export default class ServerConfig extends React.PureComponent { // TODO: Do we want to support .well-known lookups here? // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to // find their homeserver without demanding they use "https://matrix.org" - return this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl); + const result = this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl); + if (!result) { + return result; + } + + // If the UI flow this component is embedded in requires an identity + // server when the homeserver says it will need one, check first and + // reveal this field if not already shown. + // XXX: This a backward compatibility path for homeservers that require + // an identity server to be passed during certain flows. + // See also https://github.com/matrix-org/synapse/pull/5868. + if ( + this.props.showIdentityServerIfRequiredByHomeserver && + !this.state.showIdentityServer && + await this.isIdentityServerRequiredByHomeserver() + ) { + this.setState({ + showIdentityServer: true, + }); + return null; + } + + return result; } async validateAndApplyServer(hsUrl, isUrl) { @@ -126,6 +156,15 @@ export default class ServerConfig extends React.PureComponent { } } + async isIdentityServerRequiredByHomeserver() { + // XXX: We shouldn't have to create a whole new MatrixClient just to + // check if the homeserver requires an identity server... Should it be + // extracted to a static utils function...? + return createClient({ + baseUrl: this.state.hsUrl, + }).doesServerRequireIdServerParam(); + } + onHomeserverBlur = (ev) => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { this.validateServer(); @@ -171,8 +210,49 @@ export default class ServerConfig extends React.PureComponent { Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); }; - render() { + _renderHomeserverSection() { const Field = sdk.getComponent('elements.Field'); + return
+ {_t("Enter your custom homeserver URL What does this mean?", {}, { + a: sub => + {sub} + , + })} + +
; + } + + _renderIdentityServerSection() { + const Field = sdk.getComponent('elements.Field'); + const classes = classNames({ + "mx_ServerConfig_identityServer": true, + "mx_ServerConfig_identityServer_shown": this.state.showIdentityServer, + }); + return
+ {_t("Enter your custom identity server URL What does this mean?", {}, { + a: sub => + {sub} + , + })} + +
; + } + + render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const errorText = this.state.errorText @@ -191,31 +271,10 @@ export default class ServerConfig extends React.PureComponent { return (

{_t("Other servers")}

- {_t("Enter custom server URLs What does this mean?", {}, { - a: sub => - { sub } - , - })} {errorText} + {this._renderHomeserverSection()} + {this._renderIdentityServerSection()} -
- - -
{submitButton}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 83a9602a51..f90bdc8bb5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1461,8 +1461,9 @@ "Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.", "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.", "Other servers": "Other servers", - "Enter custom server URLs What does this mean?": "Enter custom server URLs What does this mean?", + "Enter your custom homeserver URL What does this mean?": "Enter your custom homeserver URL What does this mean?", "Homeserver URL": "Homeserver URL", + "Enter your custom identity server URL What does this mean?": "Enter your custom identity server URL What does this mean?", "Identity Server URL": "Identity Server URL", "Free": "Free", "Join millions for free on the largest public server": "Join millions for free on the largest public server", From c44ae2df4d47b6fb5787cdc50ca01dbf7e12aa1d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Aug 2019 11:58:04 -0600 Subject: [PATCH 232/413] Treat 404 errors on IS as having no terms Fixes https://github.com/vector-im/riot-web/issues/10634 --- src/components/views/settings/SetIdServer.js | 46 ++++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index e7aa22527d..ddd246d928 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -153,31 +153,17 @@ export default class SetIdServer extends React.Component { // Double check that the identity server even has terms of service. const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { - const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); - Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { - title: _t("Identity server has no terms of service"), - description: ( -
- - {_t("The identity server you have chosen does not have any terms of service.")} - - -  {_t("Only continue if you trust the owner of the server.")} - -
- ), - button: _t("Continue"), - onFinished: async (confirmed) => { - if (!confirmed) return; - this._saveIdServer(fullUrl); - }, - }); + this._showNoTermsWarning(fullUrl); return; } this._saveIdServer(fullUrl); } catch (e) { console.error(e); + if (e.cors === "rejected" || e.httpStatus === 404) { + this._showNoTermsWarning(fullUrl); + return; + } errStr = _t("Terms of service not accepted or the identity server is invalid."); } } @@ -190,6 +176,28 @@ export default class SetIdServer extends React.Component { }); }; + _showNoTermsWarning(fullUrl) { + const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); + Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { + title: _t("Identity server has no terms of service"), + description: ( +
+ + {_t("The identity server you have chosen does not have any terms of service.")} + + +  {_t("Only continue if you trust the owner of the server.")} + +
+ ), + button: _t("Continue"), + onFinished: async (confirmed) => { + if (!confirmed) return; + this._saveIdServer(fullUrl); + }, + }); + }; + _onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); try { From e1552b61fcece1c776f8ce0bc2e96bca1b2b9a3d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Aug 2019 12:01:13 -0600 Subject: [PATCH 233/413] fix i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 83a9602a51..a0c98a0335 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -549,10 +549,10 @@ "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", "Could not connect to Identity Server": "Could not connect to Identity Server", "Checking server": "Checking server", + "Terms of service not accepted or the identity server is invalid.": "Terms of service not accepted or the identity server is invalid.", "Identity server has no terms of service": "Identity server has no terms of service", "The identity server you have chosen does not have any terms of service.": "The identity server you have chosen does not have any terms of service.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", - "Terms of service not accepted or the identity server is invalid.": "Terms of service not accepted or the identity server is invalid.", "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect Identity Server": "Disconnect Identity Server", From e8b0c411578d84363b105e7f76e7e4e9b0be7237 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Aug 2019 12:01:47 -0600 Subject: [PATCH 234/413] minus ; --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ddd246d928..ba405a5652 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -196,7 +196,7 @@ export default class SetIdServer extends React.Component { this._saveIdServer(fullUrl); }, }); - }; + } _onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); From accb0abe2dbef874aa6e5c80777e75d3db60d828 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 24 Aug 2019 11:47:07 +0100 Subject: [PATCH 235/413] Switch from react-addons-test-utils to react-dom/test-utils. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 1 - test/components/structures/MessagePanel-test.js | 2 +- test/components/views/dialogs/InteractiveAuthDialog-test.js | 2 +- .../components/views/elements/MemberEventListSummary-test.js | 2 +- test/components/views/rooms/MemberList-test.js | 2 +- test/components/views/rooms/MessageComposerInput-test.js | 5 ++--- test/components/views/rooms/RoomList-test.js | 2 +- yarn.lock | 5 ----- 8 files changed, 7 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index ffd701a233..b4e1af9f0a 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,6 @@ "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "mocha": "^5.0.5", - "react-addons-test-utils": "^15.4.0", "require-json": "0.0.1", "rimraf": "^2.4.3", "sinon": "^5.0.7", diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 138681457c..58b1590cf1 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -19,7 +19,7 @@ import SettingsStore from "../../../src/settings/SettingsStore"; const React = require('react'); const ReactDOM = require("react-dom"); import PropTypes from "prop-types"; -const TestUtils = require('react-addons-test-utils'); +const TestUtils = require('react-dom/test-utils'); const expect = require('expect'); import sinon from 'sinon'; import { EventEmitter } from "events"; diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 95f76dfd3e..b14ea7c242 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -18,7 +18,7 @@ import expect from 'expect'; import Promise from 'bluebird'; import React from 'react'; import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import sinon from 'sinon'; import MatrixReactTestUtils from 'matrix-react-test-utils'; diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index f3749b850f..d1e112735d 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -1,6 +1,6 @@ import expect from 'expect'; import React from 'react'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import sdk from 'matrix-react-sdk'; import * as languageHandler from '../../../../src/languageHandler'; import * as testUtils from '../../../test-utils'; diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index b9d96635a2..9a1439c2f7 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import expect from 'expect'; import lolex from 'lolex'; diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index ed07c0f233..1105a4af17 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import expect from 'expect'; import sinon from 'sinon'; @@ -8,7 +8,6 @@ import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); import MatrixClientPeg from '../../../../src/MatrixClientPeg'; -import RoomMember from 'matrix-js-sdk'; function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); @@ -301,4 +300,4 @@ xdescribe('MessageComposerInput', () => { expect(spy.args[0][1].body).toEqual('[Click here](https://some.lovely.url)'); expect(spy.args[0][1].formatted_body).toEqual('Click here'); }); -}); \ No newline at end of file +}); diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index 754367cd23..68168fcf29 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import expect from 'expect'; import lolex from 'lolex'; diff --git a/yarn.lock b/yarn.lock index b9341b2a0e..c664d0b7dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6349,11 +6349,6 @@ react-addons-css-transition-group@15.3.2: resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.3.2.tgz#d8fa52bec9bb61bdfde8b9e4652b80297cbff667" integrity sha1-2PpSvsm7Yb396LnkZSuAKXy/9mc= -react-addons-test-utils@^15.4.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156" - integrity sha1-wStu/cIkfBDae4dw0YUICnsEcVY= - react-beautiful-dnd@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81" From 360cef66c1b3e61978bc8f7fe5397a8419fe2d2a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 24 Aug 2019 11:53:28 +0100 Subject: [PATCH 236/413] Migrate away from React.createClass for async-components. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/async-components/views/dialogs/EncryptedEventDialog.js | 3 ++- src/async-components/views/dialogs/ExportE2eKeysDialog.js | 3 ++- src/async-components/views/dialogs/ImportE2eKeysDialog.js | 3 ++- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index 5db8b2365f..145203136a 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -15,12 +15,13 @@ limitations under the License. */ const React = require("react"); +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; const sdk = require('../../../index'); const MatrixClientPeg = require("../../../MatrixClientPeg"); -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'EncryptedEventDialog', propTypes: { diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 529780c121..0fd412935a 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -17,6 +17,7 @@ limitations under the License. import FileSaver from 'file-saver'; import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import { MatrixClient } from 'matrix-js-sdk'; @@ -26,7 +27,7 @@ import sdk from '../../../index'; const PHASE_EDIT = 1; const PHASE_EXPORTING = 2; -export default React.createClass({ +export default createReactClass({ displayName: 'ExportE2eKeysDialog', propTypes: { diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 5181b6da2f..17f3bba117 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; @@ -37,7 +38,7 @@ function readFileAsArrayBuffer(file) { const PHASE_EDIT = 1; const PHASE_IMPORTING = 2; -export default React.createClass({ +export default createReactClass({ displayName: 'ImportE2eKeysDialog', propTypes: { diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 9ceff69467..e36763591e 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; @@ -48,7 +49,7 @@ function selectText(target) { * Walks the user through the process of creating an e2e key backup * on the server. */ -export default React.createClass({ +export default createReactClass({ getInitialState: function() { return { phase: PHASE_PASSPHRASE, From d94e2179bfcf919f7b557a9ad1e4e78074cedac9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 24 Aug 2019 11:59:46 +0100 Subject: [PATCH 237/413] Migrate away from React.createClass for views/dialogs. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/AddressPickerDialog.js | 3 ++- src/components/views/dialogs/AskInviteAnywayDialog.js | 3 ++- src/components/views/dialogs/BaseDialog.js | 3 ++- src/components/views/dialogs/ConfirmRedactDialog.js | 3 ++- src/components/views/dialogs/ConfirmUserActionDialog.js | 3 ++- src/components/views/dialogs/CreateGroupDialog.js | 3 ++- src/components/views/dialogs/CreateRoomDialog.js | 3 ++- src/components/views/dialogs/ErrorDialog.js | 3 ++- src/components/views/dialogs/InfoDialog.js | 3 ++- src/components/views/dialogs/InteractiveAuthDialog.js | 3 ++- src/components/views/dialogs/KeyShareDialog.js | 3 ++- src/components/views/dialogs/QuestionDialog.js | 3 ++- src/components/views/dialogs/RoomUpgradeDialog.js | 3 ++- src/components/views/dialogs/SessionRestoreErrorDialog.js | 3 ++- src/components/views/dialogs/SetEmailDialog.js | 3 ++- src/components/views/dialogs/SetMxIdDialog.js | 3 ++- src/components/views/dialogs/SetPasswordDialog.js | 3 ++- src/components/views/dialogs/TextInputDialog.js | 3 ++- src/components/views/dialogs/UnknownDeviceDialog.js | 5 ++--- .../views/dialogs/keybackup/RestoreKeyBackupDialog.js | 3 ++- 20 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 6cb5a278fd..ac2181f1f2 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -19,6 +19,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t, _td } from '../../../languageHandler'; import sdk from '../../../index'; @@ -39,7 +40,7 @@ const addressTypeName = { }; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: "AddressPickerDialog", propTypes: { diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js index d4b073eb01..3d10752ff8 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.js @@ -16,12 +16,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore"; -export default React.createClass({ +export default createReactClass({ propTypes: { unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ] onInviteAnyways: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index ee838b9825..65b89d1631 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import FocusTrap from 'focus-trap-react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -32,7 +33,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; * Includes a div for the title, and a keypress handler which cancels the * dialog on escape. */ -export default React.createClass({ +export default createReactClass({ displayName: 'BaseDialog', propTypes: { diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js index a967b5df9a..c606706ed2 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; /* * A dialog for confirming a redaction. */ -export default React.createClass({ +export default createReactClass({ displayName: 'ConfirmRedactDialog', render: function() { diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 4848e468e9..4d33b2b500 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; @@ -29,7 +30,7 @@ import { GroupMemberType } from '../../../groups'; * to make it obvious what is going to happen. * Also tweaks the style for 'dangerous' actions (albeit only with colour) */ -export default React.createClass({ +export default createReactClass({ displayName: 'ConfirmUserActionDialog', propTypes: { // matrix-js-sdk (room) member object. Supply either this or 'groupMember' diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 882d323449..11f4c21366 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; -export default React.createClass({ +export default createReactClass({ displayName: 'CreateGroupDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index 3212e53c05..e1da9f841d 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -15,12 +15,13 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'CreateRoomDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index a055f07629..f6db0a14a5 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -26,11 +26,12 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'ErrorDialog', propTypes: { title: PropTypes.string, diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index d01b737309..c54da480e6 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -17,12 +17,13 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import classNames from "classnames"; -export default React.createClass({ +export default createReactClass({ displayName: 'InfoDialog', propTypes: { className: PropTypes.string, diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index b068428bed..0b658bad81 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; @@ -23,7 +24,7 @@ import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; -export default React.createClass({ +export default createReactClass({ displayName: 'InteractiveAuthDialog', propTypes: { diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index b9b64a69d2..a10c25a0fb 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -16,6 +16,7 @@ limitations under the License. import Modal from '../../../Modal'; import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; @@ -29,7 +30,7 @@ import { _t, _td } from '../../../languageHandler'; * should not, and `undefined` if the dialog is cancelled. (In other words: * truthy: do the key share. falsy: don't share the keys). */ -export default React.createClass({ +export default createReactClass({ propTypes: { matrixClient: PropTypes.object.isRequired, userId: PropTypes.string.isRequired, diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 4d0defadc2..4d2a699898 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -16,11 +16,12 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'QuestionDialog', propTypes: { title: PropTypes.string, diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js index 45c242fea5..6900ac6fe8 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'RoomUpgradeDialog', propTypes: { diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index f7e117b31b..b9f6e77222 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; @@ -23,7 +24,7 @@ import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'SessionRestoreErrorDialog', propTypes: { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index e643ddbc34..88baa5fd3e 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import Email from '../../../email'; @@ -29,7 +30,7 @@ import Modal from '../../../Modal'; * * On success, `onFinished(true)` is called. */ -export default React.createClass({ +export default createReactClass({ displayName: 'SetEmailDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index dfaff52278..3bc6f5597e 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -17,6 +17,7 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -34,7 +35,7 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250; * * On success, `onFinished(true, newDisplayName)` is called. */ -export default React.createClass({ +export default createReactClass({ displayName: 'SetMxIdDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/SetPasswordDialog.js b/src/components/views/dialogs/SetPasswordDialog.js index 0ec933b59f..0fe65aaca3 100644 --- a/src/components/views/dialogs/SetPasswordDialog.js +++ b/src/components/views/dialogs/SetPasswordDialog.js @@ -17,6 +17,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -62,7 +63,7 @@ const WarmFuzzy = function(props) { * * On success, `onFinished()` when finished */ -export default React.createClass({ +export default createReactClass({ displayName: 'SetPasswordDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index f28b16ef6f..3ce32ef4ec 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -15,10 +15,11 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -export default React.createClass({ +export default createReactClass({ displayName: 'TextInputDialog', propTypes: { title: PropTypes.string, diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index 09b967c72f..e7522e971d 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -16,11 +16,10 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import GeminiScrollbar from 'react-gemini-scrollbar'; -import Resend from '../../../Resend'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import { markAllDevicesKnown } from '../../../cryptodevices'; @@ -67,7 +66,7 @@ UnknownDeviceList.propTypes = { }; -export default React.createClass({ +export default createReactClass({ displayName: 'UnknownDeviceDialog', propTypes: { diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 0f390a02c9..172a3ed9ea 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import Modal from '../../../../Modal'; @@ -29,7 +30,7 @@ const RESTORE_TYPE_RECOVERYKEY = 1; /** * Dialog for restoring e2e keys from a backup and the user's recovery key */ -export default React.createClass({ +export default createReactClass({ getInitialState: function() { return { backupInfo: null, From 10291bafe0a72a3ed492324b5b32b2e12d6ab029 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 23 Aug 2019 17:37:58 +0100 Subject: [PATCH 238/413] add support for selecting ranges in the editor model, and replacing them this to support finding emoticons and replacing them with an emoji --- src/editor/model.js | 39 +++++++++++------ src/editor/position.js | 38 +++++++++++++++++ src/editor/range.js | 53 +++++++++++++++++++++++ test/editor/range-test.js | 88 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 src/editor/position.js create mode 100644 src/editor/range.js create mode 100644 test/editor/range-test.js diff --git a/src/editor/model.js b/src/editor/model.js index 2f1e5218d8..c5b80247f6 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -16,6 +16,8 @@ limitations under the License. */ import {diffAtCaret, diffDeletion} from "./diff"; +import DocumentPosition from "./position"; +import Range from "./range"; export default class EditorModel { constructor(parts, partCreator, updateCallback = null) { @@ -197,7 +199,7 @@ export default class EditorModel { this._updateCallback(pos); } - _mergeAdjacentParts(docPos) { + _mergeAdjacentParts() { let prevPart; for (let i = 0; i < this._parts.length; ++i) { let part = this._parts[i]; @@ -339,19 +341,32 @@ export default class EditorModel { return new DocumentPosition(index, totalOffset - currentOffset); } -} -class DocumentPosition { - constructor(index, offset) { - this._index = index; - this._offset = offset; + startRange(position) { + return new Range(this, position); } - get index() { - return this._index; - } - - get offset() { - return this._offset; + // called from Range.replace + replaceRange(startPosition, endPosition, parts) { + const newStartPartIndex = this._splitAt(startPosition); + const idxDiff = newStartPartIndex - startPosition.index; + // if both position are in the same part, and we split it at start position, + // the offset of the end position needs to be decreased by the offset of the start position + const removedOffset = startPosition.index === endPosition.index ? startPosition.offset : 0; + const adjustedEndPosition = new DocumentPosition( + endPosition.index + idxDiff, + endPosition.offset - removedOffset, + ); + const newEndPartIndex = this._splitAt(adjustedEndPosition); + for (let i = newEndPartIndex - 1; i >= newStartPartIndex; --i) { + this._removePart(i); + } + let insertIdx = newStartPartIndex; + for (const part of parts) { + this._insertPart(insertIdx, part); + insertIdx += 1; + } + this._mergeAdjacentParts(); + this._updateCallback(); } } diff --git a/src/editor/position.js b/src/editor/position.js new file mode 100644 index 0000000000..c771482012 --- /dev/null +++ b/src/editor/position.js @@ -0,0 +1,38 @@ +/* +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 default class DocumentPosition { + constructor(index, offset) { + this._index = index; + this._offset = offset; + } + + get index() { + return this._index; + } + + get offset() { + return this._offset; + } + + compare(otherPos) { + if (this._index === otherPos._index) { + return this._offset - otherPos._offset; + } else { + return this._index - otherPos._index; + } + } +} diff --git a/src/editor/range.js b/src/editor/range.js new file mode 100644 index 0000000000..e2ecc5d12b --- /dev/null +++ b/src/editor/range.js @@ -0,0 +1,53 @@ +/* +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 default class Range { + constructor(model, startPosition, endPosition = startPosition) { + this._model = model; + this._start = startPosition; + this._end = endPosition; + } + + moveStart(delta) { + this._start = this._start.forwardsWhile(this._model, () => { + delta -= 1; + return delta >= 0; + }); + } + + expandBackwardsWhile(predicate) { + this._start = this._start.backwardsWhile(this._model, predicate); + } + + get text() { + let text = ""; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + const t = part.text.substring(startIdx, endIdx); + text = text + t; + }); + return text; + } + + replace(parts) { + const newLength = parts.reduce((sum, part) => sum + part.text.length, 0); + let oldLength = 0; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + oldLength += endIdx - startIdx; + }); + this._model.replaceRange(this._start, this._end, parts); + return newLength - oldLength; + } +} diff --git a/test/editor/range-test.js b/test/editor/range-test.js new file mode 100644 index 0000000000..5a95da952d --- /dev/null +++ b/test/editor/range-test.js @@ -0,0 +1,88 @@ +/* +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. +*/ + +import expect from 'expect'; +import EditorModel from "../../src/editor/model"; +import {createPartCreator} from "./mock"; + +function createRenderer() { + const render = (c) => { + render.caret = c; + render.count += 1; + }; + render.count = 0; + render.caret = null; + return render; +} + +const pillChannel = "#riot-dev:matrix.org"; + +describe('editor/range', function() { + it('range on empty model', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([], pc, renderer); + const range = model.startRange(model.positionForOffset(0, true)); // after "world" + let called = false; + range.expandBackwardsWhile(chr => { + called = true; + return true; + }); + expect(called).toBe(false); + expect(range.text).toBe(""); + }); + it('range replace within a part', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello world!!!!")], pc, renderer); + const range = model.startRange(model.positionForOffset(11)); // after "world" + range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); + expect(range.text).toBe("world"); + range.replace([pc.roomPill(pillChannel)]); + console.log({parts: JSON.stringify(model.serializeParts())}); + expect(model.parts[0].type).toBe("plain"); + expect(model.parts[0].text).toBe("hello "); + expect(model.parts[1].type).toBe("room-pill"); + expect(model.parts[1].text).toBe(pillChannel); + expect(model.parts[2].type).toBe("plain"); + expect(model.parts[2].text).toBe("!!!!"); + expect(model.parts.length).toBe(3); + expect(renderer.count).toBe(1); + }); + it('range replace across parts', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("try to re"), + pc.plain("pla"), + pc.plain("ce "), + pc.plain("me"), + ], pc, renderer); + const range = model.startRange(model.positionForOffset(14)); // after "replace" + range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); + expect(range.text).toBe("replace"); + console.log("range.text", {text: range.text}); + range.replace([pc.roomPill(pillChannel)]); + expect(model.parts[0].type).toBe("plain"); + expect(model.parts[0].text).toBe("try to "); + expect(model.parts[1].type).toBe("room-pill"); + expect(model.parts[1].text).toBe(pillChannel); + expect(model.parts[2].type).toBe("plain"); + expect(model.parts[2].text).toBe(" me"); + expect(model.parts.length).toBe(3); + expect(renderer.count).toBe(1); + }); +}); From 0e65f71a375fbdee100828dcd84cd6c22a448796 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:00:56 +0200 Subject: [PATCH 239/413] support incrementing/decrementing doc positions with predicate --- src/editor/position.js | 69 +++++++++++++++++++++++++++++++ test/editor/position-test.js | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 test/editor/position-test.js diff --git a/src/editor/position.js b/src/editor/position.js index c771482012..5dcb31fe65 100644 --- a/src/editor/position.js +++ b/src/editor/position.js @@ -35,4 +35,73 @@ export default class DocumentPosition { return this._index - otherPos._index; } } + + iteratePartsBetween(other, model, callback) { + if (this.index === -1 || other.index === -1) { + return; + } + const [startPos, endPos] = this.compare(other) < 0 ? [this, other] : [other, this]; + if (startPos.index === endPos.index) { + callback(model.parts[this.index], startPos.offset, endPos.offset); + } else { + const firstPart = model.parts[startPos.index]; + callback(firstPart, startPos.offset, firstPart.text.length); + for (let i = startPos.index + 1; i < endPos.index; ++i) { + const part = model.parts[i]; + callback(part, 0, part.text.length); + } + const lastPart = model.parts[endPos.index]; + callback(lastPart, 0, endPos.offset); + } + } + + forwardsWhile(model, predicate) { + if (this.index === -1) { + return this; + } + + let {index, offset} = this; + const {parts} = model; + while (index < parts.length) { + const part = parts[index]; + while (offset < part.text.length) { + if (!predicate(index, offset, part)) { + return new DocumentPosition(index, offset); + } + offset += 1; + } + // end reached + if (index === (parts.length - 1)) { + return new DocumentPosition(index, offset); + } else { + index += 1; + offset = 0; + } + } + } + + backwardsWhile(model, predicate) { + if (this.index === -1) { + return this; + } + + let {index, offset} = this; + const parts = model.parts; + while (index >= 0) { + const part = parts[index]; + while (offset > 0) { + if (!predicate(index, offset - 1, part)) { + return new DocumentPosition(index, offset); + } + offset -= 1; + } + // start reached + if (index === 0) { + return new DocumentPosition(index, offset); + } else { + index -= 1; + offset = parts[index].text.length; + } + } + } } diff --git a/test/editor/position-test.js b/test/editor/position-test.js new file mode 100644 index 0000000000..7ac4284c60 --- /dev/null +++ b/test/editor/position-test.js @@ -0,0 +1,80 @@ +/* +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. +*/ + +import expect from 'expect'; +import EditorModel from "../../src/editor/model"; +import {createPartCreator} from "./mock"; + +function createRenderer() { + const render = (c) => { + render.caret = c; + render.count += 1; + }; + render.count = 0; + render.caret = null; + return render; +} + +describe('editor/position', function() { + it('move first position backward in empty model', function() { + const model = new EditorModel([], createPartCreator(), createRenderer()); + const pos = model.positionForOffset(0, true); + const pos2 = pos.backwardsWhile(model, () => true); + expect(pos).toBe(pos2); + }); + it('move first position forwards in empty model', function() { + const model = new EditorModel([], createPartCreator(), createRenderer()); + const pos = model.positionForOffset(0, true); + const pos2 = pos.forwardsWhile(() => true); + expect(pos).toBe(pos2); + }); + it('move forwards within one part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello")], pc, createRenderer()); + const pos = model.positionForOffset(1); + let n = 3; + const pos2 = pos.forwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(0); + expect(pos2.offset).toBe(4); + }); + it('move forwards crossing to other part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello"), pc.plain(" world")], pc, createRenderer()); + const pos = model.positionForOffset(4); + let n = 3; + const pos2 = pos.forwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(1); + expect(pos2.offset).toBe(2); + }); + it('move backwards within one part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello")], pc, createRenderer()); + const pos = model.positionForOffset(4); + let n = 3; + const pos2 = pos.backwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(0); + expect(pos2.offset).toBe(1); + }); + it('move backwards crossing to other part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello"), pc.plain(" world")], pc, createRenderer()); + const pos = model.positionForOffset(7); + let n = 3; + const pos2 = pos.backwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(0); + expect(pos2.offset).toBe(4); + }); +}); From f8f0e77bdefbc8cbc2de0def3d78c5de0eec0123 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:09:46 +0200 Subject: [PATCH 240/413] add transform step during editor model update --- src/editor/model.js | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index c5b80247f6..5584627688 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -19,6 +19,15 @@ import {diffAtCaret, diffDeletion} from "./diff"; import DocumentPosition from "./position"; import Range from "./range"; + /** + * @callback TransformCallback + * @param {DocumentPosition?} caretPosition the position where the caret should be position + * @param {string?} inputType the inputType of the DOM input event + * @param {object?} diff an object with `removed` and `added` strings + * @return {Number?} addedLen how many characters were added/removed (-) before the caret during the transformation step. + This is used to adjust the caret position. + */ + export default class EditorModel { constructor(parts, partCreator, updateCallback = null) { this._parts = parts; @@ -26,7 +35,19 @@ export default class EditorModel { this._activePartIdx = null; this._autoComplete = null; this._autoCompletePartIdx = null; + this._transformCallback = null; this.setUpdateCallback(updateCallback); + this._updateInProgress = false; + } + + /** Set a callback for the transformation step. + * While processing an update, right before calling the update callback, + * a transform callback can be called, which serves to do modifications + * on the model that can span multiple parts. Also see `startRange()`. + * @param {TransformCallback} transformCallback + */ + setTransformCallback(transformCallback) { + this._transformCallback = transformCallback; } setUpdateCallback(updateCallback) { @@ -133,6 +154,7 @@ export default class EditorModel { } update(newValue, inputType, caret) { + this._updateInProgress = true; const diff = this._diff(newValue, inputType, caret); const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; @@ -147,11 +169,23 @@ export default class EditorModel { } this._mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; - const newPosition = this.positionForOffset(caretOffset, true); + let newPosition = this.positionForOffset(caretOffset, true); this._setActivePart(newPosition, canOpenAutoComplete); + if (this._transformCallback) { + const transformAddedLen = this._transform(newPosition, inputType, diff); + if (transformAddedLen !== 0) { + newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); + } + } + this._updateInProgress = false; this._updateCallback(newPosition, inputType, diff); } + _transform(newPosition, inputType, diff) { + const result = this._transformCallback(newPosition, inputType, diff); + return Number.isFinite(result) ? result : 0; + } + _setActivePart(pos, canOpenAutoComplete) { const {index} = pos; const part = this._parts[index]; @@ -367,6 +401,8 @@ export default class EditorModel { insertIdx += 1; } this._mergeAdjacentParts(); - this._updateCallback(); + if (!this._updateInProgress) { + this._updateCallback(); + } } } From 4fd4ad41c127061c7b5440f72dc3fcd3eae721ee Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:10:02 +0200 Subject: [PATCH 241/413] improve editor model documentation --- src/editor/model.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/editor/model.js b/src/editor/model.js index 5584627688..689b657f05 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -19,6 +19,13 @@ import {diffAtCaret, diffDeletion} from "./diff"; import DocumentPosition from "./position"; import Range from "./range"; +/** + * @callback ModelCallback + * @param {DocumentPosition?} caretPosition the position where the caret should be position + * @param {string?} inputType the inputType of the DOM input event + * @param {object?} diff an object with `removed` and `added` strings + */ + /** * @callback TransformCallback * @param {DocumentPosition?} caretPosition the position where the caret should be position @@ -50,6 +57,9 @@ export default class EditorModel { this._transformCallback = transformCallback; } + /** Set a callback for rerendering the model after it has been updated. + * @param {ModelCallback} updateCallback + */ setUpdateCallback(updateCallback) { this._updateCallback = updateCallback; } @@ -376,6 +386,11 @@ export default class EditorModel { return new DocumentPosition(index, totalOffset - currentOffset); } + /** + * Starts a range, which can span across multiple parts, to find and replace text. + * @param {DocumentPosition} position where to start the range + * @return {Range} + */ startRange(position) { return new Range(this, position); } From 0273795f5db86c416a2e40978c5b853a3fae26c8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:10:26 +0200 Subject: [PATCH 242/413] add transform step to composer to auto-replace emoticons with emoji --- .../views/rooms/BasicMessageComposer.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index e4179d9c3b..780f39f9e7 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -25,6 +25,11 @@ import {autoCompleteCreator} from '../../../editor/parts'; import {renderModel} from '../../../editor/render'; import {Room} from 'matrix-js-sdk'; import TypingStore from "../../../stores/TypingStore"; +import EMOJIBASE from 'emojibase-data/en/compact.json'; +import SettingsStore from "../../../settings/SettingsStore"; +import EMOTICON_REGEX from 'emojibase-regex/emoticon'; + +const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); const IS_MAC = navigator.platform.indexOf("Mac") !== -1; @@ -70,6 +75,28 @@ export default class BasicMessageEditor extends React.Component { this._modifiedFlag = false; } + _replaceEmoticon = (caret, inputType, diff) => { + const {model} = this.props; + const range = model.startRange(caret); + // expand range max 8 characters backwards from caret + let n = 8; + range.expandBackwardsWhile((index, offset) => { + const part = model.parts[index]; + n -= 1; + return n >= 0 && (part.type === "plain" || part.type === "pill-candidate"); + }); + const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); + if (emoticonMatch) { + const query = emoticonMatch[1].toLowerCase().replace("-", ""); + const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); + if (data) { + // + 1 because index is reported without preceding space + range.moveStart(emoticonMatch.index + 1); + return range.replace([this.props.model.partCreator.plain(data.unicode + " ")]); + } + } + } + _updateEditorState = (caret, inputType, diff) => { renderModel(this._editorRef, this.props.model); if (caret) { @@ -262,6 +289,9 @@ export default class BasicMessageEditor extends React.Component { componentDidMount() { const model = this.props.model; model.setUpdateCallback(this._updateEditorState); + if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { + model.setTransformCallback(this._replaceEmoticon); + } const partCreator = model.partCreator; // TODO: does this allow us to get rid of EditorStateTransfer? // not really, but we could not serialize the parts, and just change the autoCompleter From abbc8ffef06dfde98102bc3a02024d5065c03e62 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 26 Aug 2019 11:25:50 -0600 Subject: [PATCH 243/413] Adjust copy and include identity server changing when terms are pending Fixes https://github.com/vector-im/riot-web/issues/10636 Fixes https://github.com/vector-im/riot-web/issues/10635 --- src/components/views/settings/SetIdServer.js | 33 ++++++++++++++++--- .../tabs/user/GeneralUserSettingsTab.js | 21 ++++++++---- src/i18n/strings/en_EN.json | 3 ++ 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ba405a5652..55dc3b6e94 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -16,6 +16,7 @@ limitations under the License. import url from 'url'; import React from 'react'; +import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import sdk from '../../../index'; import MatrixClientPeg from "../../../MatrixClientPeg"; @@ -55,6 +56,12 @@ async function checkIdentityServerUrl(u) { } export default class SetIdServer extends React.Component { + static propTypes = { + // Whether or not the ID server is missing terms. This affects the text + // shown to the user. + missingTerms: PropTypes.bool, + }; + constructor() { super(); @@ -274,6 +281,13 @@ export default class SetIdServer extends React.Component { {}, { server: sub => {abbreviateUrl(idServerUrl)} }, ); + if (this.props.missingTerms) { + bodyText = _t( + "If you don't want to use to discover and be discoverable by existing " + + "contacts you know, enter another identity server below.", + {}, {server: sub => {abbreviateUrl(idServerUrl)}}, + ); + } } else { sectionTitle = _t("Identity Server"); bodyText = _t( @@ -286,16 +300,25 @@ export default class SetIdServer extends React.Component { let discoSection; if (idServerUrl) { let discoButtonContent = _t("Disconnect"); + let discoBodyText = _t( + "Disconnecting from your identity server will mean you " + + "won't be discoverable by other users and you won't be " + + "able to invite others by email or phone.", + ); + if (this.props.missingTerms) { + discoBodyText = _t( + "Using an identity server is optional. If you choose not to " + + "use an identity server, you won't be discoverable by other users " + + "and you won't be able to invite others by email or phone.", + ); + discoButtonContent = _t("Do not use an identity server"); + } if (this.state.disconnectBusy) { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); discoButtonContent = ; } discoSection =
- {_t( - "Disconnecting from your identity server will mean you " + - "won't be discoverable by other users and you won't be " + - "able to invite others by email or phone.", - )} + {discoBodyText} {discoButtonContent} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 3d78afd222..9c37730fc5 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -249,6 +249,8 @@ export default class GeneralUserSettingsTab extends React.Component { } _renderDiscoverySection() { + const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); + if (this.state.requiredPolicyInfo.hasTerms) { const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement"); const intro = @@ -258,17 +260,22 @@ export default class GeneralUserSettingsTab extends React.Component { {serverName: this.state.idServerName}, )} ; - return ; + return ( +
+ + { /* has its own heading as it includes the current ID server */ } + +
+ ); } const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses"); const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers"); - const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); const threepidSection = this.state.haveIdServer ?
{_t("Email addresses")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a0c98a0335..1c3c75ebd5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -559,9 +559,12 @@ "Disconnect": "Disconnect", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.", "Identity Server": "Identity Server", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.", + "Do not use an identity server": "Do not use an identity server", "Enter a new identity server": "Enter a new identity server", "Change": "Change", "Failed to update integration manager": "Failed to update integration manager", From 5c28b57681e0ff75194a15cd180000f3886e481c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 09:49:22 +0200 Subject: [PATCH 244/413] always recalculate position after doing transform step as the amount of characters might not have changed, parts may still have been merged, removed or added which requires a new position. --- src/editor/model.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index 689b657f05..d0f1be7158 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -183,9 +183,7 @@ export default class EditorModel { this._setActivePart(newPosition, canOpenAutoComplete); if (this._transformCallback) { const transformAddedLen = this._transform(newPosition, inputType, diff); - if (transformAddedLen !== 0) { - newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); - } + newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); } this._updateInProgress = false; this._updateCallback(newPosition, inputType, diff); From 56606a46f4b120065957ca020122e7da6edc5bc2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 09:50:36 +0200 Subject: [PATCH 245/413] don't assume preceding space for emoticon at start of document also add more inline comments to explain what is going on --- src/components/views/rooms/BasicMessageComposer.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 780f39f9e7..662167b714 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -78,7 +78,8 @@ export default class BasicMessageEditor extends React.Component { _replaceEmoticon = (caret, inputType, diff) => { const {model} = this.props; const range = model.startRange(caret); - // expand range max 8 characters backwards from caret + // expand range max 8 characters backwards from caret, + // as a space to look for an emoticon let n = 8; range.expandBackwardsWhile((index, offset) => { const part = model.parts[index]; @@ -90,8 +91,14 @@ export default class BasicMessageEditor extends React.Component { const query = emoticonMatch[1].toLowerCase().replace("-", ""); const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); if (data) { - // + 1 because index is reported without preceding space - range.moveStart(emoticonMatch.index + 1); + const hasPrecedingSpace = emoticonMatch[0][0] === " "; + // we need the range to only comprise of the emoticon + // because we'll replace the whole range with an emoji, + // so move the start forward to the start of the emoticon. + // Take + 1 because index is reported without the possible preceding space. + range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0)); + // this returns the amount of added/removed characters during the replace + // so the caret position can be adjusted. return range.replace([this.props.model.partCreator.plain(data.unicode + " ")]); } } From f10e1d76549b1cf7972ec951741521088739ebd3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 09:54:13 +0200 Subject: [PATCH 246/413] fix jsdoc comments --- src/editor/model.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index d0f1be7158..9d129afa69 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -32,7 +32,7 @@ import Range from "./range"; * @param {string?} inputType the inputType of the DOM input event * @param {object?} diff an object with `removed` and `added` strings * @return {Number?} addedLen how many characters were added/removed (-) before the caret during the transformation step. - This is used to adjust the caret position. + * This is used to adjust the caret position. */ export default class EditorModel { @@ -47,7 +47,8 @@ export default class EditorModel { this._updateInProgress = false; } - /** Set a callback for the transformation step. + /** + * Set a callback for the transformation step. * While processing an update, right before calling the update callback, * a transform callback can be called, which serves to do modifications * on the model that can span multiple parts. Also see `startRange()`. @@ -57,7 +58,8 @@ export default class EditorModel { this._transformCallback = transformCallback; } - /** Set a callback for rerendering the model after it has been updated. + /** + * Set a callback for rerendering the model after it has been updated. * @param {ModelCallback} updateCallback */ setUpdateCallback(updateCallback) { From 713205e0ab11f05eabffaae71face5d130ccb5b3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:10:11 +0200 Subject: [PATCH 247/413] close autocomplete when removing auto-completed part --- src/editor/model.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index 9d129afa69..7f87bdea23 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -90,10 +90,14 @@ export default class EditorModel { _removePart(index) { this._parts.splice(index, 1); - if (this._activePartIdx >= index) { + if (index === this._activePartIdx) { + this._activePartIdx = null; + } else if (this._activePartIdx > index) { --this._activePartIdx; } - if (this._autoCompletePartIdx >= index) { + if (index === this._autoCompletePartIdx) { + this._autoCompletePartIdx = null; + } else if (this._autoCompletePartIdx > index) { --this._autoCompletePartIdx; } } From 0f6465a1dbc1af6efde1638bbcd565bdceb6d0a2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:11:18 +0200 Subject: [PATCH 248/413] don't close autocomplete when hitting tab that's not what the slate impl does and it's not an improvement --- src/editor/autocomplete.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index ac662c32d8..cf3082ce13 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -52,9 +52,6 @@ export default class AutocompleteWrapperModel { } else { await acComponent.moveSelection(e.shiftKey ? -1 : +1); } - this._updateCallback({ - close: true, - }); } onUpArrow() { From f76a23d5dd3c78985c86bf9e255c7f8b19b47a33 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:12:44 +0200 Subject: [PATCH 249/413] return promise from updating autocomplete so one can await if needed --- src/components/views/rooms/BasicMessageComposer.js | 2 +- src/editor/model.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 662167b714..ad0e76c3e4 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -304,7 +304,7 @@ export default class BasicMessageEditor extends React.Component { // not really, but we could not serialize the parts, and just change the autoCompleter partCreator.setAutoCompleteCreator(autoCompleteCreator( () => this._autocompleteRef, - query => this.setState({query}), + query => new Promise(resolve => this.setState({query}, resolve)), )); this.historyManager = new HistoryManager(partCreator); // initial render of model diff --git a/src/editor/model.js b/src/editor/model.js index 7f87bdea23..b020bd8fb5 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -186,13 +186,14 @@ export default class EditorModel { this._mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; let newPosition = this.positionForOffset(caretOffset, true); - this._setActivePart(newPosition, canOpenAutoComplete); + const acPromise = this._setActivePart(newPosition, canOpenAutoComplete); if (this._transformCallback) { const transformAddedLen = this._transform(newPosition, inputType, diff); newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); } this._updateInProgress = false; this._updateCallback(newPosition, inputType, diff); + return acPromise; } _transform(newPosition, inputType, diff) { @@ -218,13 +219,14 @@ export default class EditorModel { } // not _autoComplete, only there if active part is autocomplete part if (this.autoComplete) { - this.autoComplete.onPartUpdate(part, pos.offset); + return this.autoComplete.onPartUpdate(part, pos.offset); } } else { this._activePartIdx = null; this._autoComplete = null; this._autoCompletePartIdx = null; } + return Promise.resolve(); } _onAutoComplete = ({replacePart, caretOffset, close}) => { From 68c2bb7ca60caccc67d146fbfcaa05c2fb3ec114 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:15:10 +0200 Subject: [PATCH 250/413] introduce `transform` method so update can be called with a position and also for multiple transformations at once. This removes the need to call the update callback from `replaceRange()` as well --- src/editor/model.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index b020bd8fb5..4c657f3168 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -35,6 +35,11 @@ import Range from "./range"; * This is used to adjust the caret position. */ +/** + * @callback ManualTransformCallback + * @return the caret position + */ + export default class EditorModel { constructor(parts, partCreator, updateCallback = null) { this._parts = parts; @@ -44,7 +49,6 @@ export default class EditorModel { this._autoCompletePartIdx = null; this._transformCallback = null; this.setUpdateCallback(updateCallback); - this._updateInProgress = false; } /** @@ -170,7 +174,6 @@ export default class EditorModel { } update(newValue, inputType, caret) { - this._updateInProgress = true; const diff = this._diff(newValue, inputType, caret); const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; @@ -191,7 +194,6 @@ export default class EditorModel { const transformAddedLen = this._transform(newPosition, inputType, diff); newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); } - this._updateInProgress = false; this._updateCallback(newPosition, inputType, diff); return acPromise; } @@ -422,8 +424,18 @@ export default class EditorModel { insertIdx += 1; } this._mergeAdjacentParts(); - if (!this._updateInProgress) { - this._updateCallback(); - } + } + + /** + * Performs a transformation not part of an update cycle. + * Modifying the model should only happen inside a transform call if not part of an update call. + * @param {ManualTransformCallback} callback to run the transformations in + * @return {Promise} a promise when auto-complete (if applicable) is done updating + */ + transform(callback) { + const pos = callback(); + const acPromise = this._setActivePart(pos, true); + this._updateCallback(pos); + return acPromise; } } From f02713d08eac01dba489f572f63899a400c508f7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:16:43 +0200 Subject: [PATCH 251/413] force completion when hitting tab by replacing word before caret with pill-candidate and forcing auto complete --- .../views/rooms/BasicMessageComposer.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index ad0e76c3e4..10fa676989 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -269,6 +269,9 @@ export default class BasicMessageEditor extends React.Component { default: return; // don't preventDefault on anything else } + } else if (event.key === "Tab") { + this._tabCompleteName(event); + handled = true; } } if (handled) { @@ -277,6 +280,22 @@ export default class BasicMessageEditor extends React.Component { } } + async _tabCompleteName(event) { + const {model} = this.props; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + const range = model.startRange(position); + range.expandBackwardsWhile((index, offset, part) => { + return part.text[offset] !== " " && (part.type === "plain" || part.type === "pill-candidate"); + }); + const {partCreator} = model; + await model.transform(() => { + const addedLen = range.replace([partCreator.pillCandidate(range.text)]); + return model.positionForOffset(caret.offset + addedLen, true); + }); + await model.autoComplete.onTab(); + } + isModified() { return this._modifiedFlag; } From f5bb872efa6623bab3fe03036be87471a8f0ea3b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:17:41 +0200 Subject: [PATCH 252/413] some cleanup --- src/components/views/rooms/BasicMessageComposer.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 10fa676989..4aa622b6c2 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -75,10 +75,10 @@ export default class BasicMessageEditor extends React.Component { this._modifiedFlag = false; } - _replaceEmoticon = (caret, inputType, diff) => { + _replaceEmoticon = (caretPosition, inputType, diff) => { const {model} = this.props; - const range = model.startRange(caret); - // expand range max 8 characters backwards from caret, + const range = model.startRange(caretPosition); + // expand range max 8 characters backwards from caretPosition, // as a space to look for an emoticon let n = 8; range.expandBackwardsWhile((index, offset) => { @@ -91,6 +91,7 @@ export default class BasicMessageEditor extends React.Component { const query = emoticonMatch[1].toLowerCase().replace("-", ""); const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); if (data) { + const {partCreator} = model; const hasPrecedingSpace = emoticonMatch[0][0] === " "; // we need the range to only comprise of the emoticon // because we'll replace the whole range with an emoji, @@ -99,7 +100,7 @@ export default class BasicMessageEditor extends React.Component { range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0)); // this returns the amount of added/removed characters during the replace // so the caret position can be adjusted. - return range.replace([this.props.model.partCreator.plain(data.unicode + " ")]); + return range.replace([partCreator.plain(data.unicode + " ")]); } } } @@ -160,7 +161,7 @@ export default class BasicMessageEditor extends React.Component { } _refreshLastCaretIfNeeded() { - // TODO: needed when going up and down in editing messages ... not sure why yet + // XXX: needed when going up and down in editing messages ... not sure why yet // because the editors should stop doing this when when blurred ... // maybe it's on focus and the _editorRef isn't available yet or something. if (!this._editorRef) { From e0ec827a64eacb70cb45dc4540f69f136f07fca5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:18:09 +0200 Subject: [PATCH 253/413] extra docs --- src/editor/parts.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/editor/parts.js b/src/editor/parts.js index f9b4243de4..bc420ecde1 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -366,6 +366,8 @@ export class PartCreator { constructor(room, client, autoCompleteCreator = null) { this._room = room; this._client = client; + // pre-create the creator as an object even without callback so it can already be passed + // to PillCandidatePart (e.g. while deserializing) and set later on this._autoCompleteCreator = {create: autoCompleteCreator && autoCompleteCreator(this)}; } From 8e66d382deee95f1e2bb82a1171d23846e39aeae Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:18:22 +0200 Subject: [PATCH 254/413] don't crash on race with room members and initial composer render not ideal, but for now this prevents a crash at startup when a user-pill is persisted in local storage --- src/editor/parts.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/editor/parts.js b/src/editor/parts.js index bc420ecde1..8d0fe36c28 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -284,6 +284,9 @@ class UserPillPart extends PillPart { } setAvatar(node) { + if (!this._member) { + return; + } const name = this._member.name || this._member.userId; const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this._member.userId); let avatarUrl = Avatar.avatarUrlForMember( From d8bb9ecedfa4f0e8d7d87dea6b4f5c6fe59ab1c0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:37:04 +0200 Subject: [PATCH 255/413] bring insert method inline with transform callback, add docs before the insertPartsAt method would call the update callback on its own, but now we have the concept of a transformation session, so lets bring the API in line --- .../views/rooms/SendMessageComposer.js | 19 +++++++++++++++---- src/editor/model.js | 17 ++++++++++------- src/editor/range.js | 6 ++++++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index c8fac0b667..698356a175 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -279,22 +279,33 @@ export default class SendMessageComposer extends React.Component { }; _insertMention(userId) { + const {model} = this; + const {partCreator} = model; const member = this.props.room.getMember(userId); const displayName = member ? member.rawDisplayName : userId; - const userPillPart = this.model.partCreator.userPill(displayName, userId); - this.model.insertPartsAt([userPillPart], this._editorRef.getCaret()); + const userPillPart = partCreator.userPill(displayName, userId); + const caret = this._editorRef.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + model.transform(() => { + const addedLen = model.insert([userPillPart], position); + return model.positionForOffset(caret.offset + addedLen, true); + }); // refocus on composer, as we just clicked "Mention" this._editorRef && this._editorRef.focus(); } _insertQuotedMessage(event) { - const {partCreator} = this.model; + const {model} = this; + const {partCreator} = model; const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true }); // add two newlines quoteParts.push(partCreator.newline()); quoteParts.push(partCreator.newline()); - this.model.insertPartsAt(quoteParts, {offset: 0}); + model.transform(() => { + const addedLen = model.insert(quoteParts, model.positionForOffset(0)); + return model.positionForOffset(addedLen, true); + }); // refocus on composer, as we just clicked "Quote" this._editorRef && this._editorRef.focus(); } diff --git a/src/editor/model.js b/src/editor/model.js index 4c657f3168..ca04d9fdd0 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -158,8 +158,14 @@ export default class EditorModel { this._updateCallback(caret, inputType); } - insertPartsAt(parts, caret) { - const position = this.positionForOffset(caret.offset, caret.atNodeEnd); + /** + * Inserts the given parts at the given position. + * Should be run inside a `model.transform()` callback. + * @param {Part[]} parts the parts to replace the range with + * @param {DocumentPosition} position the position to start inserting at + * @return {Number} the amount of characters added + */ + insert(parts, position) { const insertIndex = this._splitAt(position); let newTextLength = 0; for (let i = 0; i < parts.length; ++i) { @@ -167,10 +173,7 @@ export default class EditorModel { newTextLength += part.text.length; this._insertPart(insertIndex + i, part); } - // put caret after new part - const lastPartIndex = insertIndex + parts.length - 1; - const newPosition = new DocumentPosition(lastPartIndex, newTextLength); - this._updateCallback(newPosition); + return newTextLength; } update(newValue, inputType, caret) { @@ -403,7 +406,7 @@ export default class EditorModel { return new Range(this, position); } - // called from Range.replace + //mostly internal, called from Range.replace replaceRange(startPosition, endPosition, parts) { const newStartPartIndex = this._splitAt(startPosition); const idxDiff = newStartPartIndex - startPosition.index; diff --git a/src/editor/range.js b/src/editor/range.js index e2ecc5d12b..1aaf480733 100644 --- a/src/editor/range.js +++ b/src/editor/range.js @@ -41,6 +41,12 @@ export default class Range { return text; } + /** + * Splits the model at the range boundaries and replaces with the given parts. + * Should be run inside a `model.transform()` callback. + * @param {Part[]} parts the parts to replace the range with + * @return {Number} the net amount of characters added, can be negative. + */ replace(parts) { const newLength = parts.reduce((sum, part) => sum + part.text.length, 0); let oldLength = 0; From 994bcb5c85058ffbe071976c1c3620eca74d8a00 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 16:39:30 +0200 Subject: [PATCH 256/413] dont expect rendered to be called from `range.replace()` anymore as this is now called from the `transform` method, unused in this test --- test/editor/range-test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/editor/range-test.js b/test/editor/range-test.js index 5a95da952d..e5fa48ea15 100644 --- a/test/editor/range-test.js +++ b/test/editor/range-test.js @@ -60,7 +60,6 @@ describe('editor/range', function() { expect(model.parts[2].type).toBe("plain"); expect(model.parts[2].text).toBe("!!!!"); expect(model.parts.length).toBe(3); - expect(renderer.count).toBe(1); }); it('range replace across parts', function() { const renderer = createRenderer(); @@ -83,6 +82,5 @@ describe('editor/range', function() { expect(model.parts[2].type).toBe("plain"); expect(model.parts[2].text).toBe(" me"); expect(model.parts.length).toBe(3); - expect(renderer.count).toBe(1); }); }); From ac2b8b874f2cc66e0eb6cc5b92539addc37aa050 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 27 Aug 2019 17:29:56 -0400 Subject: [PATCH 257/413] Don't infinite loop on server change ServerConfig assumed that the state was already correct when checking the given urls against the default, but that is not neccessarily the case (eg. the validation can return a different url to what the user entered). This would cause an infinite loop because it would keep firing onServerConfigChange to change to the desired URLs but the state would never change. Fixes part of https://github.com/vector-im/riot-web/issues/10666 --- src/components/views/auth/ServerConfig.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index 467ba307d0..8197afd512 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -82,7 +82,12 @@ export default class ServerConfig extends React.PureComponent { // Always try and use the defaults first const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"]; if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) { - this.setState({busy: false, errorText: ""}); + this.setState({ + hsUrl: defaultConfig.hsUrl, + isUrl: defaultConfig.isUrl, + busy: false, + errorText: "", + }); this.props.onServerConfigChange(defaultConfig); return defaultConfig; } From ac6b03551adf81574ab7666acbc6222c9525bf0c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 28 Aug 2019 11:24:11 +0100 Subject: [PATCH 258/413] Describe props default Co-Authored-By: Travis Ralston --- src/components/views/auth/ServerConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index 7d7a99a79c..427bb73e78 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -51,7 +51,7 @@ export default class ServerConfig extends React.PureComponent { submitClass: PropTypes.string, // Whether the flow this component is embedded in requires an identity - // server when the homeserver says it will need one. + // server when the homeserver says it will need one. Default false. showIdentityServerIfRequiredByHomeserver: PropTypes.bool, }; From 7000b5a5adead76beffee8f0ad7b9983ecf63eac Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 28 Aug 2019 11:32:36 +0100 Subject: [PATCH 259/413] Update i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f90bdc8bb5..5a450bed81 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1460,11 +1460,11 @@ "Use an email address to recover your account.": "Use an email address to recover your account.", "Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.", "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.", - "Other servers": "Other servers", "Enter your custom homeserver URL What does this mean?": "Enter your custom homeserver URL What does this mean?", "Homeserver URL": "Homeserver URL", "Enter your custom identity server URL What does this mean?": "Enter your custom identity server URL What does this mean?", "Identity Server URL": "Identity Server URL", + "Other servers": "Other servers", "Free": "Free", "Join millions for free on the largest public server": "Join millions for free on the largest public server", "Premium": "Premium", From 66714b29afbdf5d5ea73f0c26fdaaca116a4fd6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Aug 2019 11:37:20 +0100 Subject: [PATCH 260/413] expose power level toggle for enabling e2ee to room settings Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/settings/tabs/room/RolesRoomSettingsTab.js | 2 ++ src/i18n/strings/en_EN.json | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js index c4b1ae8ddc..e269c6d2cd 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js @@ -30,6 +30,7 @@ const plEventsToLabels = { "m.room.history_visibility": _td("Change history visibility"), "m.room.power_levels": _td("Change permissions"), "m.room.topic": _td("Change topic"), + "m.room.encryption": _td("Enable room encryption"), "im.vector.modular.widgets": _td("Modify widgets"), }; @@ -42,6 +43,7 @@ const plEventsToShow = { "m.room.history_visibility": {isState: true}, "m.room.power_levels": {isState: true}, "m.room.topic": {isState: true}, + "m.room.encryption": {isState: true}, "im.vector.modular.widgets": {isState: true}, }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 62b6467b94..9ed4d4c4d9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -680,6 +680,7 @@ "Change history visibility": "Change history visibility", "Change permissions": "Change permissions", "Change topic": "Change topic", + "Enable room encryption": "Enable room encryption", "Modify widgets": "Modify widgets", "Failed to unban": "Failed to unban", "Unban": "Unban", From f70f983c8cf524b32a5f0c0baf7413cff0ab93de Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Aug 2019 12:00:37 +0100 Subject: [PATCH 261/413] Expose upgrade room permissions in room settings and fix command Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/SlashCommands.js | 21 ++++++++++++------- .../tabs/room/RolesRoomSettingsTab.js | 2 ++ src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 72ace22cb6..5ed1adb40f 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -139,8 +139,13 @@ export const CommandMap = { description: _td('Upgrades a room to a new version'), runFn: function(roomId, args) { if (args) { - const room = MatrixClientPeg.get().getRoom(roomId); - Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation', + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(roomId); + if (!room.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) { + return reject(_t("You do not have the required permissions to use this command.")); + } + + const {finished} = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation', QuestionDialog, { title: _t('Room upgrade confirmation'), description: ( @@ -198,13 +203,13 @@ export const CommandMap = {
), button: _t("Upgrade"), - onFinished: (confirm) => { - if (!confirm) return; - - MatrixClientPeg.get().upgradeRoom(roomId, args); - }, }); - return success(); + + return success(finished.then((confirm) => { + if (!confirm) return; + + return cli.upgradeRoom(roomId, args); + })); } return reject(this.getUsage()); }, diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js index c4b1ae8ddc..581c4314bc 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js @@ -30,6 +30,7 @@ const plEventsToLabels = { "m.room.history_visibility": _td("Change history visibility"), "m.room.power_levels": _td("Change permissions"), "m.room.topic": _td("Change topic"), + "m.room.tombstone": _td("Upgrade the room"), "im.vector.modular.widgets": _td("Modify widgets"), }; @@ -42,6 +43,7 @@ const plEventsToShow = { "m.room.history_visibility": {isState: true}, "m.room.power_levels": {isState: true}, "m.room.topic": {isState: true}, + "m.room.tombstone": {isState: true}, "im.vector.modular.widgets": {isState: true}, }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 62b6467b94..661a775781 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -146,6 +146,7 @@ "/ddg is not a command": "/ddg is not a command", "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", "Upgrades a room to a new version": "Upgrades a room to a new version", + "You do not have the required permissions to use this command.": "You do not have the required permissions to use this command.", "Room upgrade confirmation": "Room upgrade confirmation", "Upgrading a room can be destructive and isn't always necessary.": "Upgrading a room can be destructive and isn't always necessary.", "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.", @@ -680,6 +681,7 @@ "Change history visibility": "Change history visibility", "Change permissions": "Change permissions", "Change topic": "Change topic", + "Upgrade the room": "Upgrade the room", "Modify widgets": "Modify widgets", "Failed to unban": "Failed to unban", "Unban": "Unban", From 591fa3d8c559dd1963a9a07f3a96f3a658b3ab97 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Aug 2019 14:46:47 +0100 Subject: [PATCH 262/413] Don't use cursor: pointer on roomsettings avatar if you can't change it Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/settings/_ProfileSettings.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index 3e97a0ff6d..432b713c1b 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -43,7 +43,6 @@ limitations under the License. height: 88px; margin-left: 13px; position: relative; - cursor: pointer; } .mx_ProfileSettings_avatar > * { @@ -71,6 +70,7 @@ limitations under the License. text-align: center; vertical-align: middle; font-size: 10px; + cursor: pointer; } .mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlay:not(.mx_ProfileSettings_avatarOverlay_disabled) { From c44fbb73d0d2f27415330eb172613cc4cc93b9c5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 28 Aug 2019 15:52:39 +0200 Subject: [PATCH 263/413] fix bug when replacing range starting at end of previous part --- src/editor/model.js | 15 ++++++--------- src/editor/offset.js | 26 ++++++++++++++++++++++++++ src/editor/position.js | 16 ++++++++++++++++ test/editor/range-test.js | 20 ++++++++++++++++++-- 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 src/editor/offset.js diff --git a/src/editor/model.js b/src/editor/model.js index ca04d9fdd0..59371cc3e6 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -408,16 +408,13 @@ export default class EditorModel { //mostly internal, called from Range.replace replaceRange(startPosition, endPosition, parts) { + // convert end position to offset, so it is independent of how the document is split into parts + // which we'll change when splitting up at the start position + const endOffset = endPosition.asOffset(this); const newStartPartIndex = this._splitAt(startPosition); - const idxDiff = newStartPartIndex - startPosition.index; - // if both position are in the same part, and we split it at start position, - // the offset of the end position needs to be decreased by the offset of the start position - const removedOffset = startPosition.index === endPosition.index ? startPosition.offset : 0; - const adjustedEndPosition = new DocumentPosition( - endPosition.index + idxDiff, - endPosition.offset - removedOffset, - ); - const newEndPartIndex = this._splitAt(adjustedEndPosition); + // convert it back to position once split at start + endPosition = endOffset.asPosition(this); + const newEndPartIndex = this._splitAt(endPosition); for (let i = newEndPartIndex - 1; i >= newStartPartIndex; --i) { this._removePart(i); } diff --git a/src/editor/offset.js b/src/editor/offset.js new file mode 100644 index 0000000000..7054836bdc --- /dev/null +++ b/src/editor/offset.js @@ -0,0 +1,26 @@ +/* +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 default class DocumentOffset { + constructor(offset, atEnd) { + this.offset = offset; + this.atEnd = atEnd; + } + + asPosition(model) { + return model.positionForOffset(this.offset, this.atEnd); + } +} diff --git a/src/editor/position.js b/src/editor/position.js index 5dcb31fe65..98b158e547 100644 --- a/src/editor/position.js +++ b/src/editor/position.js @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import DocumentOffset from "./offset"; + export default class DocumentPosition { constructor(index, offset) { this._index = index; @@ -104,4 +106,18 @@ export default class DocumentPosition { } } } + + asOffset(model) { + if (this.index === -1) { + return new DocumentOffset(0, true); + } + let offset = 0; + for (let i = 0; i < this.index; ++i) { + offset += model.parts[i].text.length; + } + offset += this.offset; + const lastPart = model.parts[this.index]; + const atEnd = offset >= lastPart.text.length; + return new DocumentOffset(offset, atEnd); + } } diff --git a/test/editor/range-test.js b/test/editor/range-test.js index e5fa48ea15..468cb60c76 100644 --- a/test/editor/range-test.js +++ b/test/editor/range-test.js @@ -52,7 +52,6 @@ describe('editor/range', function() { range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); expect(range.text).toBe("world"); range.replace([pc.roomPill(pillChannel)]); - console.log({parts: JSON.stringify(model.serializeParts())}); expect(model.parts[0].type).toBe("plain"); expect(model.parts[0].text).toBe("hello "); expect(model.parts[1].type).toBe("room-pill"); @@ -73,7 +72,6 @@ describe('editor/range', function() { const range = model.startRange(model.positionForOffset(14)); // after "replace" range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); expect(range.text).toBe("replace"); - console.log("range.text", {text: range.text}); range.replace([pc.roomPill(pillChannel)]); expect(model.parts[0].type).toBe("plain"); expect(model.parts[0].text).toBe("try to "); @@ -83,4 +81,22 @@ describe('editor/range', function() { expect(model.parts[2].text).toBe(" me"); expect(model.parts.length).toBe(3); }); + // bug found while implementing tab completion + it('replace a part with an identical part with start position at end of previous part', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("hello "), + pc.pillCandidate("man"), + ], pc, renderer); + const range = model.startRange(model.positionForOffset(9, true)); // before "man" + range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); + expect(range.text).toBe("man"); + range.replace([pc.pillCandidate(range.text)]); + expect(model.parts[0].type).toBe("plain"); + expect(model.parts[0].text).toBe("hello "); + expect(model.parts[1].type).toBe("pill-candidate"); + expect(model.parts[1].text).toBe("man"); + expect(model.parts.length).toBe(2); + }); }); From 85efb71a23033842f5a540f7e1d4a0ed2ac2a847 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 28 Aug 2019 15:53:16 +0200 Subject: [PATCH 264/413] add visual bell when no replacements are available also add try/catch in _tabCompleteName so errors don't get swallowed --- .../views/rooms/_BasicMessageComposer.scss | 9 +++ .../views/rooms/BasicMessageComposer.js | 56 +++++++++++++------ src/editor/autocomplete.js | 6 +- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index b6035e5859..a4b5bb51d0 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -27,6 +27,15 @@ limitations under the License. white-space: nowrap; } + @keyframes visualbell { + from { background-color: #faa; } + to { background-color: $primary-bg-color; } + } + + &.mx_BasicMessageComposer_input_error { + animation: 0.2s visualbell; + } + .mx_BasicMessageComposer_input { white-space: pre-wrap; word-wrap: break-word; diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 4aa622b6c2..19304ec557 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -14,6 +14,8 @@ 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 classNames from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; import EditorModel from '../../../editor/model'; @@ -271,7 +273,7 @@ export default class BasicMessageEditor extends React.Component { return; // don't preventDefault on anything else } } else if (event.key === "Tab") { - this._tabCompleteName(event); + this._tabCompleteName(); handled = true; } } @@ -281,20 +283,30 @@ export default class BasicMessageEditor extends React.Component { } } - async _tabCompleteName(event) { - const {model} = this.props; - const caret = this.getCaret(); - const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - const range = model.startRange(position); - range.expandBackwardsWhile((index, offset, part) => { - return part.text[offset] !== " " && (part.type === "plain" || part.type === "pill-candidate"); - }); - const {partCreator} = model; - await model.transform(() => { - const addedLen = range.replace([partCreator.pillCandidate(range.text)]); - return model.positionForOffset(caret.offset + addedLen, true); - }); - await model.autoComplete.onTab(); + async _tabCompleteName() { + try { + await new Promise(resolve => this.setState({showVisualBell: false}, resolve)); + const {model} = this.props; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + const range = model.startRange(position); + range.expandBackwardsWhile((index, offset, part) => { + return part.text[offset] !== " " && (part.type === "plain" || part.type === "pill-candidate"); + }); + const {partCreator} = model; + // await for auto-complete to be open + await model.transform(() => { + const addedLen = range.replace([partCreator.pillCandidate(range.text)]); + return model.positionForOffset(caret.offset + addedLen, true); + }); + await model.autoComplete.onTab(); + if (!model.autoComplete.hasSelection()) { + this.setState({showVisualBell: true}); + model.autoComplete.close(); + } + } catch (err) { + console.error(err); + } } isModified() { @@ -324,7 +336,14 @@ export default class BasicMessageEditor extends React.Component { // not really, but we could not serialize the parts, and just change the autoCompleter partCreator.setAutoCompleteCreator(autoCompleteCreator( () => this._autocompleteRef, - query => new Promise(resolve => this.setState({query}, resolve)), + query => { + return new Promise(resolve => this.setState({query}, resolve)); + // if setState + // if (this.state.query === query) { + // return Promise.resolve(); + // } else { + // } + }, )); this.historyManager = new HistoryManager(partCreator); // initial render of model @@ -365,7 +384,10 @@ export default class BasicMessageEditor extends React.Component { />
); } - return (
+ const classes = classNames("mx_BasicMessageComposer", { + "mx_BasicMessageComposer_input_error": this.state.showVisualBell, + }); + return (
{ autoComplete }
Date: Wed, 28 Aug 2019 10:34:50 -0400 Subject: [PATCH 265/413] Update email help text Fixes https://github.com/vector-im/riot-web/issues/10674 --- src/components/views/auth/RegistrationForm.js | 36 ++++++++++++++----- src/i18n/strings/en_EN.json | 5 ++- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index cf1b074fe1..d3f275ffc3 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -444,6 +444,15 @@ module.exports = React.createClass({ return true; }, + _showPhoneNumber() { + const threePidLogin = !SdkConfig.get().disable_3pid_login; + const haveIs = Boolean(this.props.serverConfig.isUrl); + if (!threePidLogin || !haveIs || !this._authStepIsUsed('m.login.msisdn')) { + return false; + } + return true; + }, + renderEmail() { if (!this._showEmail()) { return null; @@ -490,9 +499,7 @@ module.exports = React.createClass({ }, renderPhoneNumber() { - const threePidLogin = !SdkConfig.get().disable_3pid_login; - const haveIs = Boolean(this.props.serverConfig.isUrl); - if (!threePidLogin || !haveIs || !this._authStepIsUsed('m.login.msisdn')) { + if (!this._showPhoneNumber()) { return null; } const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); @@ -564,11 +571,24 @@ module.exports = React.createClass({ ); - const emailHelperText = this._showEmail() ?
- {_t("Use an email address to recover your account.") + " "} - {_t("Other users can invite you to rooms using your contact details.")} -
: null; - + let emailHelperText = null; + if (this._showEmail()) { + if (this._showPhoneNumber()) { + emailHelperText =
+ {_t( + "Set an email for account recovery. " + + "Use email or phone to optionally be discoverable by existing contacts.", + )} +
; + } else { + emailHelperText =
+ {_t( + "Set an email for account recovery. " + + "Use email to optionally be discoverable by existing contacts.", + )} +
; + } + } const haveIs = Boolean(this.props.serverConfig.isUrl); const noIsText = haveIs ? null :
{_t( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 62b6467b94..f1d494bd9b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1461,9 +1461,8 @@ "Phone (optional)": "Phone (optional)", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", "Create your Matrix account on ": "Create your Matrix account on ", - "Use an email address to recover your account.": "Use an email address to recover your account.", - "Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.", - "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.", + "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.", + "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.", "Other servers": "Other servers", "Enter custom server URLs What does this mean?": "Enter custom server URLs What does this mean?", "Homeserver URL": "Homeserver URL", From aa9c0b24fe65c2e282d8dad23e97b11ff9aaf1c9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 28 Aug 2019 10:37:57 -0400 Subject: [PATCH 266/413] re-run i18n --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f1d494bd9b..2930adb145 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1463,6 +1463,7 @@ "Create your Matrix account on ": "Create your Matrix account on ", "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.", "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.", + "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.", "Other servers": "Other servers", "Enter custom server URLs What does this mean?": "Enter custom server URLs What does this mean?", "Homeserver URL": "Homeserver URL", From 29f96e659aa2ba52e388fe7d6781233eef6cccdb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 28 Aug 2019 17:53:03 +0200 Subject: [PATCH 267/413] remove leftover code --- src/components/views/rooms/BasicMessageComposer.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 19304ec557..49815c6f23 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -336,14 +336,7 @@ export default class BasicMessageEditor extends React.Component { // not really, but we could not serialize the parts, and just change the autoCompleter partCreator.setAutoCompleteCreator(autoCompleteCreator( () => this._autocompleteRef, - query => { - return new Promise(resolve => this.setState({query}, resolve)); - // if setState - // if (this.state.query === query) { - // return Promise.resolve(); - // } else { - // } - }, + query => new Promise(resolve => this.setState({query}, resolve)), )); this.historyManager = new HistoryManager(partCreator); // initial render of model From eddaece43e871975d112735e544857d42ca3ef9d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 28 Aug 2019 18:00:57 +0200 Subject: [PATCH 268/413] add visual bell color to theme + choose better value for dark mode --- res/css/views/rooms/_BasicMessageComposer.scss | 2 +- res/css/views/rooms/_MessageComposer.scss | 2 +- res/themes/dark/css/_dark.scss | 2 ++ res/themes/light/css/_light.scss | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index a4b5bb51d0..bce0ecf325 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -28,7 +28,7 @@ limitations under the License. } @keyframes visualbell { - from { background-color: #faa; } + from { background-color: $visual-bell-bg-color; } to { background-color: $primary-bg-color; } } diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 6e17251cb0..5b4a9b764b 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -129,7 +129,7 @@ limitations under the License. } @keyframes visualbell { - from { background-color: #faa; } + from { background-color: $visual-bell-bg-color; } to { background-color: $primary-bg-color; } } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 90cd8e8558..f54d25ab29 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -146,6 +146,8 @@ $button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color $button-link-fg-color: $accent-color; $button-link-bg-color: transparent; +$visual-bell-bg-color: #800; + $room-warning-bg-color: $header-panel-bg-color; $dark-panel-bg-color: $header-panel-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index d8d4b0a11b..be46367fbb 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -247,6 +247,8 @@ $button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color $button-link-fg-color: $accent-color; $button-link-bg-color: transparent; +$visual-bell-bg-color: #faa; + // Toggle switch $togglesw-off-color: #c1c9d6; $togglesw-on-color: $accent-color; From e531b29307c2e46c6a8a21a03a1469b099d5325b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 29 Aug 2019 12:50:23 +0200 Subject: [PATCH 269/413] don't ignore BR elements when converting to editor dom to text --- src/editor/dom.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/editor/dom.js b/src/editor/dom.js index 1b683c2c5e..4f15a57303 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -84,6 +84,14 @@ function getTextAndFocusNodeOffset(editor, focusNode, focusOffset) { foundCaret = true; } } + // usually newlines are entered as new DIV elements, + // but for example while pasting in some browsers, they are still + // converted to BRs, so also take these into account when they + // are not the last element in the DIV. + if (node.tagName === "BR" && node.nextSibling) { + text += "\n"; + focusNodeOffset += 1; + } const nodeText = node.nodeType === Node.TEXT_NODE && getTextNodeValue(node); if (nodeText) { if (!foundCaret) { From 80523f5dbed3486abfe1a628e16e4905a1adb62c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 29 Aug 2019 12:51:33 +0200 Subject: [PATCH 270/413] still convert \n to NewlinePart when pasting/dropping before we skipped the complete validation (which creates NewlineParts) when pasting or dropping text. We don't want to create PillCandidatePart when inserting text like this, as it would open the auto-complete, but newlines should still be applied. So instead of skipping validation, pass the inputType to the validation code so they can only reject pill candidate characters when not pasting. --- src/editor/model.js | 22 +++++++--------------- src/editor/parts.js | 43 ++++++++++++++++++++----------------------- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index 59371cc3e6..0fbaa4bb3c 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -183,15 +183,14 @@ export default class EditorModel { if (diff.removed) { removedOffsetDecrease = this.removeText(position, diff.removed.length); } - const canOpenAutoComplete = inputType !== "insertFromPaste" && inputType !== "insertFromDrop"; let addedLen = 0; if (diff.added) { - // these shouldn't trigger auto-complete, you just want to append a piece of text - addedLen = this._addText(position, diff.added, {validate: canOpenAutoComplete}); + addedLen = this._addText(position, diff.added, inputType); } this._mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; let newPosition = this.positionForOffset(caretOffset, true); + const canOpenAutoComplete = inputType !== "insertFromPaste" && inputType !== "insertFromDrop"; const acPromise = this._setActivePart(newPosition, canOpenAutoComplete); if (this._transformCallback) { const transformAddedLen = this._transform(newPosition, inputType, diff); @@ -333,22 +332,20 @@ export default class EditorModel { * inserts `str` into the model at `pos`. * @param {Object} pos * @param {string} str - * @param {Object} options + * @param {string} inputType the source of the input, see html InputEvent.inputType * @param {bool} options.validate Whether characters will be validated by the part. * Validating allows the inserted text to be parsed according to the part rules. * @return {Number} how far from position (in characters) the insertion ended. * This can be more than the length of `str` when crossing non-editable parts, which are skipped. */ - _addText(pos, str, {validate=true}) { + _addText(pos, str, inputType) { let {index} = pos; const {offset} = pos; let addLen = str.length; const part = this._parts[index]; if (part) { if (part.canEdit) { - if (validate && part.validateAndInsert(offset, str)) { - str = null; - } else if (!validate && part.insert(offset, str)) { + if (part.validateAndInsert(offset, str, inputType)) { str = null; } else { const splitPart = part.split(offset); @@ -367,13 +364,8 @@ export default class EditorModel { index = 0; } while (str) { - const newPart = this._partCreator.createPartForInput(str, index); - if (validate) { - str = newPart.appendUntilRejected(str); - } else { - newPart.insert(0, str); - str = null; - } + const newPart = this._partCreator.createPartForInput(str, index, inputType); + str = newPart.appendUntilRejected(str, inputType); this._insertPart(index, newPart); index += 1; } diff --git a/src/editor/parts.js b/src/editor/parts.js index 8d0fe36c28..d14fcf98a2 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -23,7 +23,7 @@ class BasePart { this._text = text; } - acceptsInsertion(chr) { + acceptsInsertion(chr, offset, inputType) { return true; } @@ -56,10 +56,11 @@ class BasePart { } // append str, returns the remaining string if a character was rejected. - appendUntilRejected(str) { + appendUntilRejected(str, inputType) { + const offset = this.text.length; for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); - if (!this.acceptsInsertion(chr, i)) { + if (!this.acceptsInsertion(chr, offset + i, inputType)) { this._text = this._text + str.substr(0, i); return str.substr(i); } @@ -69,10 +70,10 @@ class BasePart { // inserts str at offset if all the characters in str were accepted, otherwise don't do anything // return whether the str was accepted or not. - validateAndInsert(offset, str) { + validateAndInsert(offset, str, inputType) { for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); - if (!this.acceptsInsertion(chr)) { + if (!this.acceptsInsertion(chr, offset + i, inputType)) { return false; } } @@ -82,16 +83,6 @@ class BasePart { return true; } - insert(offset, str) { - if (this.canEdit) { - const beforeInsert = this._text.substr(0, offset); - const afterInsert = this._text.substr(offset); - this._text = beforeInsert + str + afterInsert; - return true; - } - return false; - } - createAutoComplete() {} trim(len) { @@ -119,8 +110,15 @@ class BasePart { // exported for unit tests, should otherwise only be used through PartCreator export class PlainPart extends BasePart { - acceptsInsertion(chr) { - return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n"; + acceptsInsertion(chr, offset, inputType) { + if (chr === "\n") { + return false; + } + // when not pasting or dropping text, reject characters that should start a pill candidate + if (inputType !== "insertFromPaste" && inputType !== "insertFromDrop") { + return chr !== "@" && chr !== "#" && chr !== ":"; + } + return true; } toDOMNode() { @@ -141,7 +139,6 @@ export class PlainPart extends BasePart { updateDOMNode(node) { if (node.textContent !== this.text) { - // console.log("changing plain text from", node.textContent, "to", this.text); node.textContent = this.text; } } @@ -211,8 +208,8 @@ class PillPart extends BasePart { } class NewlinePart extends BasePart { - acceptsInsertion(chr, i) { - return (this.text.length + i) === 0 && chr === "\n"; + acceptsInsertion(chr, offset) { + return offset === 0 && chr === "\n"; } acceptsRemoval(position, chr) { @@ -331,11 +328,11 @@ class PillCandidatePart extends PlainPart { return this._autoCompleteCreator.create(updateCallback); } - acceptsInsertion(chr, i) { - if ((this.text.length + i) === 0) { + acceptsInsertion(chr, offset, inputType) { + if (offset === 0) { return true; } else { - return super.acceptsInsertion(chr, i); + return super.acceptsInsertion(chr, offset, inputType); } } From 891ccf0f4ce0850ffa596bfa50fa380263072be4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 29 Aug 2019 13:56:21 +0200 Subject: [PATCH 271/413] don't update model while doing IME composition this prevents the composition from being disrupted because the DOM is modified, and also complete compositions are added to the undo history like this. --- .../views/rooms/BasicMessageComposer.js | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 49815c6f23..a72f00c28f 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -75,6 +75,7 @@ export default class BasicMessageEditor extends React.Component { this._editorRef = null; this._autocompleteRef = null; this._modifiedFlag = false; + this._isIMEComposing = false; } _replaceEmoticon = (caretPosition, inputType, diff) => { @@ -119,11 +120,9 @@ export default class BasicMessageEditor extends React.Component { if (this.props.placeholder) { const {isEmpty} = this.props.model; if (isEmpty) { - this._editorRef.style.setProperty("--placeholder", `'${this.props.placeholder}'`); - this._editorRef.classList.add("mx_BasicMessageComposer_inputEmpty"); + this._showPlaceholder(); } else { - this._editorRef.classList.remove("mx_BasicMessageComposer_inputEmpty"); - this._editorRef.style.removeProperty("--placeholder"); + this._hidePlaceholder(); } } this.setState({autoComplete: this.props.model.autoComplete}); @@ -135,7 +134,31 @@ export default class BasicMessageEditor extends React.Component { } } + _showPlaceholder() { + this._editorRef.style.setProperty("--placeholder", `'${this.props.placeholder}'`); + this._editorRef.classList.add("mx_BasicMessageComposer_inputEmpty"); + } + + _hidePlaceholder() { + this._editorRef.classList.remove("mx_BasicMessageComposer_inputEmpty"); + this._editorRef.style.removeProperty("--placeholder"); + } + + _onCompositionStart = (event) => { + this._isIMEComposing = true; + // even if the model is empty, the composition text shouldn't be mixed with the placeholder + this._hidePlaceholder(); + } + + _onCompositionEnd = (event) => { + this._isIMEComposing = false; + } + _onInput = (event) => { + // ignore any input while doing IME compositions + if (this._isIMEComposing) { + return; + } this._modifiedFlag = true; const sel = document.getSelection(); const {caret, text} = getCaretOffsetAndText(this._editorRef, sel); @@ -323,6 +346,8 @@ export default class BasicMessageEditor extends React.Component { componentWillUnmount() { this._editorRef.removeEventListener("input", this._onInput, true); + this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true); + this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true); } componentDidMount() { @@ -344,6 +369,8 @@ export default class BasicMessageEditor extends React.Component { // attach input listener by hand so React doesn't proxy the events, // as the proxied event doesn't support inputType, which we need. this._editorRef.addEventListener("input", this._onInput, true); + this._editorRef.addEventListener("compositionstart", this._onCompositionStart, true); + this._editorRef.addEventListener("compositionend", this._onCompositionEnd, true); this._editorRef.focus(); } From fe7ac11abc1aa8417dd05e7c090b635a394f1507 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 29 Aug 2019 16:19:05 +0200 Subject: [PATCH 272/413] New composer: support pasting files --- .../views/rooms/BasicMessageComposer.js | 4 ++++ .../views/rooms/SendMessageComposer.js | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 49815c6f23..48ce81e895 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -309,6 +309,10 @@ export default class BasicMessageEditor extends React.Component { } } + getEditableRootNode() { + return this._editorRef; + } + isModified() { return this._modifiedFlag; } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 698356a175..0e03d83467 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -32,6 +32,7 @@ import {processCommandInput} from '../../../SlashCommands'; import sdk from '../../../index'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; +import ContentMessages from '../../../ContentMessages'; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -226,8 +227,13 @@ export default class SendMessageComposer extends React.Component { this._clearStoredEditorState(); } + componentDidMount() { + this._editorRef.getEditableRootNode().addEventListener("paste", this._onPaste, true); + } + componentWillUnmount() { dis.unregister(this.dispatcherRef); + this._editorRef.getEditableRootNode().removeEventListener("paste", this._onPaste, true); } componentWillMount() { @@ -310,6 +316,19 @@ export default class SendMessageComposer extends React.Component { this._editorRef && this._editorRef.focus(); } + _onPaste = (event) => { + const {clipboardData} = event; + if (clipboardData.files.length) { + // This actually not so much for 'files' as such (at time of writing + // neither chrome nor firefox let you paste a plain file copied + // from Finder) but more images copied from a different website + // / word processor etc. + ContentMessages.sharedInstance().sendContentListToRoom( + Array.from(clipboardData.files), this.props.room.roomId, this.context.matrixClient, + ); + } + } + render() { return (
From 8ff2c42f39f96e32a6f157a35a80fa7e775f3042 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2019 14:23:57 +0000 Subject: [PATCH 273/413] Bump eslint-utils from 1.4.0 to 1.4.2 Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.4.0 to 1.4.2. - [Release notes](https://github.com/mysticatea/eslint-utils/releases) - [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.4.0...v1.4.2) Signed-off-by: dependabot[bot] --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index c664d0b7dc..1989f4339a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2852,16 +2852,16 @@ eslint-scope@^4.0.0, eslint-scope@^4.0.3: estraverse "^4.1.1" eslint-utils@^1.3.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.0.tgz#e2c3c8dba768425f897cf0f9e51fe2e241485d4c" - integrity sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ== + version "1.4.2" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" + integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q== dependencies: eslint-visitor-keys "^1.0.0" eslint-visitor-keys@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" - integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" + integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== eslint@^5.12.0: version "5.16.0" From 752eb178939a5cc92f04ba33b0ae4f413aee5e47 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 28 Aug 2019 15:12:00 +0100 Subject: [PATCH 274/413] Remove subtext in room invite dialog The subtext here was deemed redundant. Part of https://github.com/vector-im/riot-web/issues/10619 --- src/RoomInvite.js | 1 - src/components/views/dialogs/AddressPickerDialog.js | 11 ++++++++--- src/i18n/strings/en_EN.json | 1 - 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index b2382e206f..7a3b59cef8 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -68,7 +68,6 @@ export function showRoomInviteDialog(roomId) { Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, { title: _t('Invite new room members'), - description: _t('Who would you like to add to this room?'), button: _t('Send Invites'), placeholder: _t("Email, name or Matrix ID"), validAddressTypes, diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index ac2181f1f2..364db97bf5 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -577,6 +577,13 @@ module.exports = createReactClass({ const AddressSelector = sdk.getComponent("elements.AddressSelector"); this.scrollElement = null; + let inputLabel; + if (this.props.description) { + inputLabel =
+ +
; + } + const query = []; // create the invite list if (this.state.selectedList.length > 0) { @@ -640,9 +647,7 @@ module.exports = createReactClass({ return ( -
- -
+ {inputLabel}
{ query }
{ error } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e024f4e2e9..513d9e64c7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -117,7 +117,6 @@ "Email, name or Matrix ID": "Email, name or Matrix ID", "Start Chat": "Start Chat", "Invite new room members": "Invite new room members", - "Who would you like to add to this room?": "Who would you like to add to this room?", "Send Invites": "Send Invites", "Failed to start chat": "Failed to start chat", "Operation failed": "Operation failed", From 166fb696c240327267f1e42fd59a1d758636a270 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 29 Aug 2019 15:20:14 +0100 Subject: [PATCH 275/413] Allow connecting to an IS from address picker This allows those who previously disconnected from an IS to either choose the default IS or a custom one from Settings via the address picker dialog. Part of https://github.com/vector-im/riot-web/issues/10619 --- res/css/_common.scss | 6 ++ res/css/views/auth/_AuthBody.scss | 3 +- .../views/dialogs/_AddressPickerDialog.scss | 3 + res/themes/dark/css/_dark.scss | 5 ++ res/themes/light/css/_light.scss | 5 ++ src/RoomInvite.js | 18 ++++- .../views/dialogs/AddressPickerDialog.js | 71 +++++++++++++++++-- src/components/views/settings/SetIdServer.js | 10 +-- src/i18n/strings/en_EN.json | 2 + src/utils/IdentityServerUtils.js | 30 ++++++++ 10 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 src/utils/IdentityServerUtils.js diff --git a/res/css/_common.scss b/res/css/_common.scss index 859c0006a1..8252d5930e 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -281,6 +281,12 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { box-shadow: 2px 15px 30px 0 $dialog-shadow-color; border-radius: 4px; overflow-y: auto; + + a:link, + a:hover, + a:visited { + @mixin mx_Dialog_link; + } } .mx_Dialog_fixedWidth { diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 49a87d8077..b05629003e 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -39,8 +39,7 @@ limitations under the License. a:link, a:hover, a:visited { - color: $accent-color; - text-decoration: none; + @mixin mx_Dialog_link; } input[type=text], diff --git a/res/css/views/dialogs/_AddressPickerDialog.scss b/res/css/views/dialogs/_AddressPickerDialog.scss index 2771ac4052..168310507c 100644 --- a/res/css/views/dialogs/_AddressPickerDialog.scss +++ b/res/css/views/dialogs/_AddressPickerDialog.scss @@ -67,3 +67,6 @@ limitations under the License. pointer-events: none; } +.mx_AddressPickerDialog_identityServer { + margin-top: 1em; +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index f54d25ab29..ef0b91b41a 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -202,6 +202,11 @@ $interactive-tooltip-fg-color: #ffffff; background-color: $button-secondary-bg-color; } +@define-mixin mx_Dialog_link { + color: $accent-color; + text-decoration: none; +} + // Nasty hacks to apply a filter to arbitrary monochrome artwork to make it // better match the theme. Typically applied to dark grey 'off' buttons or // light grey 'on' buttons. diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index be46367fbb..bfaac09761 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -328,3 +328,8 @@ $interactive-tooltip-fg-color: #ffffff; color: $accent-color; background-color: $button-secondary-bg-color; } + +@define-mixin mx_Dialog_link { + color: $accent-color; + text-decoration: none; +} diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 7a3b59cef8..856a2ca577 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -51,7 +51,14 @@ export function showStartChatInviteDialog() { Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, { title: _t('Start a chat'), description: _t("Who would you like to communicate with?"), - placeholder: _t("Email, name or Matrix ID"), + placeholder: (validAddressTypes) => { + // The set of valid address type can be mutated inside the dialog + // when you first have no IS but agree to use one in the dialog. + if (validAddressTypes.includes('email')) { + return _t("Email, name or Matrix ID"); + } + return _t("Name or Matrix ID"); + }, validAddressTypes, button: _t("Start Chat"), onFinished: _onStartDmFinished, @@ -69,7 +76,14 @@ export function showRoomInviteDialog(roomId) { Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, { title: _t('Invite new room members'), button: _t('Send Invites'), - placeholder: _t("Email, name or Matrix ID"), + placeholder: (validAddressTypes) => { + // The set of valid address type can be mutated inside the dialog + // when you first have no IS but agree to use one in the dialog. + if (validAddressTypes.includes('email')) { + return _t("Email, name or Matrix ID"); + } + return _t("Name or Matrix ID"); + }, validAddressTypes, onFinished: (shouldInvite, addrs) => { _onRoomInviteFinished(roomId, shouldInvite, addrs); diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 364db97bf5..8f0a42198e 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -24,11 +24,14 @@ import createReactClass from 'create-react-class'; import { _t, _td } from '../../../languageHandler'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import dis from '../../../dispatcher'; import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; +import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; +import { abbreviateUrl } from '../../../utils/UrlUtils'; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -49,7 +52,7 @@ module.exports = createReactClass({ // Extra node inserted after picker input, dropdown and errors extraNode: PropTypes.node, value: PropTypes.string, - placeholder: PropTypes.string, + placeholder: PropTypes.oneOfType(PropTypes.string, PropTypes.func), roomId: PropTypes.string, button: PropTypes.string, focus: PropTypes.bool, @@ -91,6 +94,9 @@ module.exports = createReactClass({ // List of UserAddressType objects representing the set of // auto-completion results for the current search query. suggestedList: [], + // List of address types initialised from props, but may change while the + // dialog is open. + validAddressTypes: this.props.validAddressTypes, }; }, @@ -101,6 +107,15 @@ module.exports = createReactClass({ } }, + getPlaceholder() { + const { placeholder } = this.props; + if (typeof placeholder === "string") { + return placeholder; + } + // Otherwise it's a function, as checked by prop types. + return placeholder(this.state.validAddressTypes); + }, + onButtonClick: function() { let selectedList = this.state.selectedList.slice(); // Check the text input field to see if user has an unconverted address @@ -434,7 +449,7 @@ module.exports = createReactClass({ // This is important, otherwise there's no way to invite // a perfectly valid address if there are close matches. const addrType = getAddressType(query); - if (this.props.validAddressTypes.includes(addrType)) { + if (this.state.validAddressTypes.includes(addrType)) { if (addrType === 'email' && !Email.looksValid(query)) { this.setState({searchError: _t("That doesn't look like a valid email address")}); return; @@ -470,7 +485,7 @@ module.exports = createReactClass({ isKnown: false, }; - if (!this.props.validAddressTypes.includes(addrType)) { + if (!this.state.validAddressTypes.includes(addrType)) { hasError = true; } else if (addrType === 'mx-user-id') { const user = MatrixClientPeg.get().getUser(addrObj.address); @@ -571,6 +586,24 @@ module.exports = createReactClass({ this._addAddressesToList(text.split(/[\s,]+/)); }, + onUseDefaultIdentityServerClick(e) { + e.preventDefault(); + + // Update the IS in account data. Actually using it may trigger terms. + useDefaultIdentityServer(); + + // Add email as a valid address type. + const { validAddressTypes } = this.state; + validAddressTypes.push('email'); + this.setState({ validAddressTypes }); + }, + + onManageSettingsClick(e) { + e.preventDefault(); + dis.dispatch({ action: 'view_user_settings' }); + this.onCancel(); + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -610,7 +643,7 @@ module.exports = createReactClass({ ref="textinput" className="mx_AddressPickerDialog_input" onChange={this.onQueryChanged} - placeholder={this.props.placeholder} + placeholder={this.getPlaceholder()} defaultValue={this.props.value} autoFocus={this.props.focus}> , @@ -621,7 +654,7 @@ module.exports = createReactClass({ let error; let addressSelector; if (this.state.invalidAddressError) { - const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t])); + const validTypeDescriptions = this.state.validAddressTypes.map((t) => _t(addressTypeName[t])); error =
{ _t("You have entered an invalid address.") }
@@ -644,6 +677,33 @@ module.exports = createReactClass({ ); } + let identityServer; + if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes('email')) { + const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); + if (defaultIdentityServerUrl) { + identityServer =
{_t( + "Use an identity server to invite by email. " + + "Use the default (%(defaultIdentityServerName)s) " + + "or manage in Settings.", + { + defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), + }, + { + default: sub => {sub}, + settings: sub => {sub}, + }, + )}
; + } else { + identityServer =
{_t( + "Use an identity server to invite by email. " + + "Manage in Settings.", + {}, { + settings: sub => {sub}, + }, + )}
; + } + } + return ( @@ -653,6 +713,7 @@ module.exports = createReactClass({ { error } { addressSelector } { this.props.extraNode } + { identityServer }
Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.", + "Use an identity server to invite by email. Manage in Settings.": "Use an identity server to invite by email. Manage in Settings.", "The following users may not exist": "The following users may not exist", "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?", "Invite anyway and never warn me again": "Invite anyway and never warn me again", diff --git a/src/utils/IdentityServerUtils.js b/src/utils/IdentityServerUtils.js new file mode 100644 index 0000000000..883bd52149 --- /dev/null +++ b/src/utils/IdentityServerUtils.js @@ -0,0 +1,30 @@ +/* +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. +*/ + +import SdkConfig from '../SdkConfig'; +import MatrixClientPeg from '../MatrixClientPeg'; + +export function getDefaultIdentityServerUrl() { + return SdkConfig.get()['validated_server_config']['isUrl']; +} + +export function useDefaultIdentityServer() { + const url = getDefaultIdentityServerUrl(); + // Account data change will update localstorage, client, etc through dispatcher + MatrixClientPeg.get().setAccountData("m.identity_server", { + base_url: url, + }); +} From 2ff2ff0e75fbd0c0d75683bc95054e0df4261107 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 29 Aug 2019 17:43:18 +0200 Subject: [PATCH 276/413] support autocomplete replacing text with multiple parts and append ": " to user pills --- src/editor/autocomplete.js | 19 ++++++++----------- src/editor/model.js | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index 79a69c07a6..67015b72e1 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -27,8 +27,7 @@ export default class AutocompleteWrapperModel { onEscape(e) { this._getAutocompleterComponent().onEscape(e); this._updateCallback({ - replacePart: this._partCreator.plain(this._queryPart.text), - caretOffset: this._queryOffset, + replaceParts: [this._partCreator.plain(this._queryPart.text)], close: true, }); } @@ -70,26 +69,24 @@ export default class AutocompleteWrapperModel { // cache the typed value and caret here // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) this._queryPart = part; - this._queryOffset = offset; return this._updateQuery(part.text); } onComponentSelectionChange(completion) { if (!completion) { this._updateCallback({ - replacePart: this._queryPart, - caretOffset: this._queryOffset, + replaceParts: [this._queryPart], }); } else { this._updateCallback({ - replacePart: this._partForCompletion(completion), + replaceParts: this._partForCompletion(completion), }); } } onComponentConfirm(completion) { this._updateCallback({ - replacePart: this._partForCompletion(completion), + replaceParts: this._partForCompletion(completion), close: true, }); } @@ -101,16 +98,16 @@ export default class AutocompleteWrapperModel { switch (firstChr) { case "@": { if (completionId === "@room") { - return this._partCreator.atRoomPill(completionId); + return [this._partCreator.atRoomPill(completionId)]; } else { - return this._partCreator.userPill(text, completionId); + return [this._partCreator.userPill(text, completionId), this._partCreator.plain(": ")]; } } case "#": - return this._partCreator.roomPill(completionId); + return [this._partCreator.roomPill(completionId)]; // used for emoji and command completion replacement default: - return this._partCreator.plain(text); + return [this._partCreator.plain(text)]; } } } diff --git a/src/editor/model.js b/src/editor/model.js index 59371cc3e6..4b8405adef 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -47,6 +47,7 @@ export default class EditorModel { this._activePartIdx = null; this._autoComplete = null; this._autoCompletePartIdx = null; + this._autoCompletePartCount = 0; this._transformCallback = null; this.setUpdateCallback(updateCallback); } @@ -219,6 +220,7 @@ export default class EditorModel { // make sure that react picks up the difference between both acs this._autoComplete = ac; this._autoCompletePartIdx = index; + this._autoCompletePartCount = 1; } } } @@ -230,23 +232,24 @@ export default class EditorModel { this._activePartIdx = null; this._autoComplete = null; this._autoCompletePartIdx = null; + this._autoCompletePartCount = 0; } return Promise.resolve(); } - _onAutoComplete = ({replacePart, caretOffset, close}) => { + _onAutoComplete = ({replaceParts, close}) => { let pos; - if (replacePart) { - this._replacePart(this._autoCompletePartIdx, replacePart); - const index = this._autoCompletePartIdx; - if (caretOffset === undefined) { - caretOffset = replacePart.text.length; - } - pos = new DocumentPosition(index, caretOffset); + if (replaceParts) { + this._parts.splice(this._autoCompletePartIdx, this._autoCompletePartCount, ...replaceParts); + this._autoCompletePartCount = replaceParts.length; + const lastPart = replaceParts[replaceParts.length - 1]; + const lastPartIndex = this._autoCompletePartIdx + replaceParts.length - 1; + pos = new DocumentPosition(lastPartIndex, lastPart.text.length); } if (close) { this._autoComplete = null; this._autoCompletePartIdx = null; + this._autoCompletePartCount = 0; } // rerender even if editor contents didn't change // to make sure the MessageEditor checks From c9572250befce53e2578ba8e21db809c112bfc26 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 29 Aug 2019 17:47:14 +0200 Subject: [PATCH 277/413] only append colon to user-pill when at start of composer by passing position to autocomplete, so completion can depend on where the pill-candidate appears. --- src/editor/autocomplete.js | 7 +++++-- src/editor/model.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index 67015b72e1..d3b8f713b9 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -65,10 +65,11 @@ export default class AutocompleteWrapperModel { this._getAutocompleterComponent().moveSelection(+1); } - onPartUpdate(part, offset) { + onPartUpdate(part, pos) { // cache the typed value and caret here // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) this._queryPart = part; + this._partIndex = pos.index; return this._updateQuery(part.text); } @@ -100,7 +101,9 @@ export default class AutocompleteWrapperModel { if (completionId === "@room") { return [this._partCreator.atRoomPill(completionId)]; } else { - return [this._partCreator.userPill(text, completionId), this._partCreator.plain(": ")]; + const pill = this._partCreator.userPill(text, completionId); + const postfix = this._partCreator.plain(this._partIndex === 0 ? ": " : " "); + return [pill, postfix]; } } case "#": diff --git a/src/editor/model.js b/src/editor/model.js index 4b8405adef..734d96393f 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -226,7 +226,7 @@ export default class EditorModel { } // not _autoComplete, only there if active part is autocomplete part if (this.autoComplete) { - return this.autoComplete.onPartUpdate(part, pos.offset); + return this.autoComplete.onPartUpdate(part, pos); } } else { this._activePartIdx = null; From be79cdddb09eb434f91b54ab60b58f0e975c0ed8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 29 Aug 2019 18:00:38 +0200 Subject: [PATCH 278/413] apply autocomplete changes to mock to fix editor unit tests --- test/editor/mock.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/editor/mock.js b/test/editor/mock.js index 7e0fd6b273..bb1a51d14b 100644 --- a/test/editor/mock.js +++ b/test/editor/mock.js @@ -40,12 +40,12 @@ class MockAutoComplete { } else { pill = this._partCreator.roomPill(match.resourceId); } - this._updateCallback({replacePart: pill, close}); + this._updateCallback({replaceParts: [pill], close}); } } // called by EditorModel when typing into pill-candidate part - onPartUpdate(part, offset) { + onPartUpdate(part, pos) { this._part = part; } } From 4ae130bd2747102e0455a2f601a0b37b8c02bb94 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Thu, 29 Aug 2019 18:13:52 +0200 Subject: [PATCH 279/413] add license header, descriptive comment and change to class --- src/components/views/elements/Spoiler.js | 36 +++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.js index 9be7bc7784..b75967b225 100644 --- a/src/components/views/elements/Spoiler.js +++ b/src/components/views/elements/Spoiler.js @@ -1,15 +1,28 @@ -'use strict'; +/* + Copyright 2019 Sorunome + + 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'; -module.exports = React.createClass({ - displayName: 'Spoiler', - - getInitialState() { - return { +export default class Spoiler extends React.Component { + constructor(props) { + super(props); + this.state = { visible: false, }; - }, + } toggleVisible(e) { if (!this.state.visible) { @@ -18,12 +31,15 @@ module.exports = React.createClass({ e.stopPropagation(); } this.setState({ visible: !this.state.visible }); - }, + } - render: function() { + render() { const reason = this.props.reason ? ( {"(" + this.props.reason + ")"} ) : null; + // react doesn't allow appending a DOM node as child. + // as such, we pass the this.props.contentHtml instead and then set the raw + // HTML content. This is secure as the contents have already been parsed previously return ( { reason } @@ -32,4 +48,4 @@ module.exports = React.createClass({ ); } -}) +} From c144edfcacc048a9428934a0b76daae35012b811 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 29 Aug 2019 18:39:35 +0200 Subject: [PATCH 280/413] dont capture enter to close autocomplete --- src/components/views/rooms/BasicMessageComposer.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 49815c6f23..f5745302d4 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -245,14 +245,6 @@ export default class BasicMessageEditor extends React.Component { if (model.autoComplete) { const autoComplete = model.autoComplete; switch (event.key) { - case "Enter": - // only capture enter when something is selected in the list, - // otherwise don't handle so the contents of the composer gets sent - if (autoComplete.hasSelection()) { - autoComplete.onEnter(event); - handled = true; - } - break; case "ArrowUp": autoComplete.onUpArrow(event); handled = true; From 2e1fb4533c67e56bd4333d854b7d254d7083c929 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Aug 2019 10:27:51 +0100 Subject: [PATCH 281/413] Migrate away from React.createClass for auth and views/auth. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/ForgotPassword.js | 3 ++- src/components/structures/auth/Login.js | 5 ++--- src/components/structures/auth/PostRegistration.js | 5 ++--- src/components/structures/auth/Registration.js | 3 ++- src/components/views/auth/AuthFooter.js | 5 ++--- src/components/views/auth/AuthHeader.js | 7 +++---- src/components/views/auth/AuthPage.js | 7 +++---- src/components/views/auth/CaptchaForm.js | 5 ++--- src/components/views/auth/CustomServerDialog.js | 3 ++- .../views/auth/InteractiveAuthEntryComponents.js | 13 +++++++------ src/components/views/auth/RegistrationForm.js | 3 ++- 11 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 11c0ff8295..de1b964ff4 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -17,6 +17,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; @@ -38,7 +39,7 @@ const PHASE_EMAIL_SENT = 3; // User has clicked the link in email and completed reset const PHASE_DONE = 4; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ForgotPassword', propTypes: { diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 31cb92d982..014fb4426d 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -16,9 +16,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import {_t, _td} from '../../../languageHandler'; import sdk from '../../../index'; @@ -54,7 +53,7 @@ _td("General failure"); /** * A wire component which glues together login UI components and Login logic */ -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'Login', propTypes: { diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 1e24d0920a..66075c80f7 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -14,15 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'PostRegistration', propTypes: { diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 2fd028ea1d..7af1eaae34 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -20,6 +20,7 @@ limitations under the License. import Matrix from 'matrix-js-sdk'; import Promise from 'bluebird'; import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; @@ -40,7 +41,7 @@ const PHASE_REGISTRATION = 1; // Enable phases for registration const PHASES_ENABLED = true; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'Registration', propTypes: { diff --git a/src/components/views/auth/AuthFooter.js b/src/components/views/auth/AuthFooter.js index 98359b9650..39d636f9cc 100644 --- a/src/components/views/auth/AuthFooter.js +++ b/src/components/views/auth/AuthFooter.js @@ -15,12 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import { _t } from '../../../languageHandler'; import React from 'react'; +import createReactClass from 'create-react-class'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'AuthFooter', render: function() { diff --git a/src/components/views/auth/AuthHeader.js b/src/components/views/auth/AuthHeader.js index f5e9e44167..193f347857 100644 --- a/src/components/views/auth/AuthHeader.js +++ b/src/components/views/auth/AuthHeader.js @@ -15,12 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'AuthHeader', render: function() { diff --git a/src/components/views/auth/AuthPage.js b/src/components/views/auth/AuthPage.js index 8cb8cf7d53..41098c9d6c 100644 --- a/src/components/views/auth/AuthPage.js +++ b/src/components/views/auth/AuthPage.js @@ -15,12 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'AuthPage', render: function() { diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js index 90f8ebe6d3..2fdfedadcf 100644 --- a/src/components/views/auth/CaptchaForm.js +++ b/src/components/views/auth/CaptchaForm.js @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; @@ -25,7 +24,7 @@ const DIV_ID = 'mx_recaptcha'; /** * A pure UI component which displays a captcha form. */ -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'CaptchaForm', propTypes: { diff --git a/src/components/views/auth/CustomServerDialog.js b/src/components/views/auth/CustomServerDialog.js index cb0ee93de9..ae1054e0d9 100644 --- a/src/components/views/auth/CustomServerDialog.js +++ b/src/components/views/auth/CustomServerDialog.js @@ -15,9 +15,10 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'CustomServerDialog', render: function() { diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 6e2c31fc55..d2eb21df23 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -17,6 +17,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import url from 'url'; import classnames from 'classnames'; @@ -63,7 +64,7 @@ import SettingsStore from "../../../settings/SettingsStore"; * focus: set the input focus appropriately in the form. */ -export const PasswordAuthEntry = React.createClass({ +export const PasswordAuthEntry = createReactClass({ displayName: 'PasswordAuthEntry', statics: { @@ -162,7 +163,7 @@ export const PasswordAuthEntry = React.createClass({ }, }); -export const RecaptchaAuthEntry = React.createClass({ +export const RecaptchaAuthEntry = createReactClass({ displayName: 'RecaptchaAuthEntry', statics: { @@ -212,7 +213,7 @@ export const RecaptchaAuthEntry = React.createClass({ }, }); -export const TermsAuthEntry = React.createClass({ +export const TermsAuthEntry = createReactClass({ displayName: 'TermsAuthEntry', statics: { @@ -351,7 +352,7 @@ export const TermsAuthEntry = React.createClass({ }, }); -export const EmailIdentityAuthEntry = React.createClass({ +export const EmailIdentityAuthEntry = createReactClass({ displayName: 'EmailIdentityAuthEntry', statics: { @@ -393,7 +394,7 @@ export const EmailIdentityAuthEntry = React.createClass({ }, }); -export const MsisdnAuthEntry = React.createClass({ +export const MsisdnAuthEntry = createReactClass({ displayName: 'MsisdnAuthEntry', statics: { @@ -540,7 +541,7 @@ export const MsisdnAuthEntry = React.createClass({ }, }); -export const FallbackAuthEntry = React.createClass({ +export const FallbackAuthEntry = createReactClass({ displayName: 'FallbackAuthEntry', propTypes: { diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index d3f275ffc3..09d349dd20 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -18,6 +18,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import Email from '../../../email'; @@ -40,7 +41,7 @@ const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from of /** * A pure UI component which displays a registration form. */ -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RegistrationForm', propTypes: { From abf111ecbdfb34620504306230386aef14a0e085 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Aug 2019 10:34:59 +0100 Subject: [PATCH 282/413] Migrate away from React.createClass for non-auth structures. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/CompatibilityPage.js | 3 ++- src/components/structures/FilePanel.js | 3 ++- src/components/structures/GroupView.js | 11 ++++++----- src/components/structures/InteractiveAuth.js | 3 ++- src/components/structures/LeftPanel.js | 5 ++--- src/components/structures/LoggedInView.js | 3 ++- src/components/structures/MatrixChat.js | 3 ++- src/components/structures/MessagePanel.js | 3 ++- src/components/structures/MyGroups.js | 3 ++- src/components/structures/NotificationPanel.js | 5 +++-- src/components/structures/RoomDirectory.js | 7 +++---- src/components/structures/RoomStatusBar.js | 5 ++--- src/components/structures/RoomSubList.js | 3 ++- src/components/structures/RoomView.js | 3 ++- src/components/structures/ScrollPanel.js | 5 +++-- src/components/structures/SearchBox.js | 3 ++- src/components/structures/TagPanel.js | 3 ++- src/components/structures/TagPanelButtons.js | 3 ++- src/components/structures/TimelinePanel.js | 7 ++++--- src/components/structures/UploadBar.js | 6 ++++-- src/components/structures/ViewSource.js | 3 ++- 21 files changed, 53 insertions(+), 37 deletions(-) diff --git a/src/components/structures/CompatibilityPage.js b/src/components/structures/CompatibilityPage.js index 9241f9e1f4..28c86f8dd8 100644 --- a/src/components/structures/CompatibilityPage.js +++ b/src/components/structures/CompatibilityPage.js @@ -16,10 +16,11 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'CompatibilityPage', propTypes: { onAccept: PropTypes.func, diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index e35a39a107..2b9594581e 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import Matrix from 'matrix-js-sdk'; @@ -25,7 +26,7 @@ import { _t } from '../../languageHandler'; /* * Component which shows the filtered file using a TimelinePanel */ -const FilePanel = React.createClass({ +const FilePanel = createReactClass({ displayName: 'FilePanel', propTypes: { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index d5fa8fa5ae..ed18f0f463 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -17,6 +17,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import Promise from 'bluebird'; import MatrixClientPeg from '../../MatrixClientPeg'; @@ -67,7 +68,7 @@ const UserSummaryType = PropTypes.shape({ }).isRequired, }); -const CategoryRoomList = React.createClass({ +const CategoryRoomList = createReactClass({ displayName: 'CategoryRoomList', props: { @@ -156,7 +157,7 @@ const CategoryRoomList = React.createClass({ }, }); -const FeaturedRoom = React.createClass({ +const FeaturedRoom = createReactClass({ displayName: 'FeaturedRoom', props: { @@ -244,7 +245,7 @@ const FeaturedRoom = React.createClass({ }, }); -const RoleUserList = React.createClass({ +const RoleUserList = createReactClass({ displayName: 'RoleUserList', props: { @@ -327,7 +328,7 @@ const RoleUserList = React.createClass({ }, }); -const FeaturedUser = React.createClass({ +const FeaturedUser = createReactClass({ displayName: 'FeaturedUser', props: { @@ -399,7 +400,7 @@ const FeaturedUser = React.createClass({ const GROUP_JOINPOLICY_OPEN = "open"; const GROUP_JOINPOLICY_INVITE = "invite"; -export default React.createClass({ +export default createReactClass({ displayName: 'GroupView', propTypes: { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index ccc906601c..5e06d124c4 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -19,13 +19,14 @@ import Matrix from 'matrix-js-sdk'; const InteractiveAuth = Matrix.InteractiveAuth; import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import {getEntryComponentForLoginType} from '../views/auth/InteractiveAuthEntryComponents'; import sdk from '../../index'; -export default React.createClass({ +export default createReactClass({ displayName: 'InteractiveAuth', propTypes: { diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 2581319d75..f083e5ab38 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { MatrixClient } from 'matrix-js-sdk'; @@ -30,7 +29,7 @@ import {_t} from "../../languageHandler"; import Analytics from "../../Analytics"; -const LeftPanel = React.createClass({ +const LeftPanel = createReactClass({ displayName: 'LeftPanel', // NB. If you add props, don't forget to update diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 17c8e91cb9..bcbf9f8155 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -18,6 +18,7 @@ limitations under the License. import { MatrixClient } from 'matrix-js-sdk'; import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { DragDropContext } from 'react-beautiful-dnd'; @@ -58,7 +59,7 @@ function canElementReceiveInput(el) { * * Components mounted below us can access the matrix client via the react context. */ -const LoggedInView = React.createClass({ +const LoggedInView = createReactClass({ displayName: 'LoggedInView', propTypes: { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index aeffff9717..d205326d0d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -20,6 +20,7 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import Matrix from "matrix-js-sdk"; @@ -106,7 +107,7 @@ const ONBOARDING_FLOW_STARTERS = [ 'view_create_group', ]; -export default React.createClass({ +export default createReactClass({ // we export this so that the integration tests can use it :-S statics: { VIEWS: VIEWS, diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 1fb0d6c725..33ae0c3f74 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -18,6 +18,7 @@ limitations under the License. /* global Velocity */ import React from 'react'; +import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -35,7 +36,7 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() /* (almost) stateless UI component which builds the event tiles in the room timeline. */ -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MessagePanel', propTypes: { diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index aec4767e7b..2de15a5444 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../index'; @@ -23,7 +24,7 @@ import { _t } from '../../languageHandler'; import dis from '../../dispatcher'; import AccessibleButton from '../views/elements/AccessibleButton'; -export default React.createClass({ +export default createReactClass({ displayName: 'MyGroups', getInitialState: function() { diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js index 50f2683138..f9ce0e008e 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.js @@ -15,7 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -const React = require('react'); +import React from 'react'; +import createReactClass from 'create-react-class'; import { _t } from '../../languageHandler'; const sdk = require('../../index'); const MatrixClientPeg = require("../../MatrixClientPeg"); @@ -23,7 +24,7 @@ const MatrixClientPeg = require("../../MatrixClientPeg"); /* * Component which shows the global notification list using a TimelinePanel */ -const NotificationPanel = React.createClass({ +const NotificationPanel = createReactClass({ displayName: 'NotificationPanel', propTypes: { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 8d8ad96ff6..aa2e56d3c2 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -15,9 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; +import createReactClass from 'create-react-class'; const MatrixClientPeg = require('../../MatrixClientPeg'); const ContentRepo = require("matrix-js-sdk").ContentRepo; @@ -39,7 +38,7 @@ function track(action) { Analytics.trackEvent('RoomDirectory', action); } -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomDirectory', propTypes: { diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 7ef080e235..21dd06767c 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -16,13 +16,12 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import Matrix from 'matrix-js-sdk'; import { _t, _td } from '../../languageHandler'; import sdk from '../../index'; -import WhoIsTyping from '../../WhoIsTyping'; import MatrixClientPeg from '../../MatrixClientPeg'; -import MemberAvatar from '../views/avatars/MemberAvatar'; import Resend from '../../Resend'; import * as cryptodevices from '../../cryptodevices'; import dis from '../../dispatcher'; @@ -39,7 +38,7 @@ function getUnsentMessages(room) { }); } -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomStatusBar', propTypes: { diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index fa74180a2c..5a90beb06f 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -17,6 +17,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import classNames from 'classnames'; import sdk from '../../index'; import dis from '../../dispatcher'; @@ -34,7 +35,7 @@ import {_t} from "../../languageHandler"; // turn this on for drop & drag console debugging galore const debug = false; -const RoomSubList = React.createClass({ +const RoomSubList = createReactClass({ displayName: 'RoomSubList', debug: debug, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 5edf19f3ef..543b35eaf8 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -24,6 +24,7 @@ limitations under the License. import shouldHideEvent from '../../shouldHideEvent'; import React from 'react'; +import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import Promise from 'bluebird'; @@ -70,7 +71,7 @@ const RoomContext = PropTypes.shape({ room: PropTypes.instanceOf(Room), }); -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomView', propTypes: { ConferenceHandler: PropTypes.any, diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 40caa627af..cb29305dd3 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -const React = require("react"); +import React from "react"; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import Promise from 'bluebird'; import { KeyCode } from '../../Keyboard'; @@ -84,7 +85,7 @@ if (DEBUG_SCROLL) { * offset as normal. */ -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ScrollPanel', propTypes: { diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index a66cfb17b6..d8ff08adbf 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -16,13 +16,14 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { KeyCode } from '../../Keyboard'; import dis from '../../dispatcher'; import { throttle } from 'lodash'; import AccessibleButton from '../../components/views/elements/AccessibleButton'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'SearchBox', propTypes: { diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index f5d1464070..a758092dc8 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import TagOrderStore from '../../stores/TagOrderStore'; @@ -28,7 +29,7 @@ import { _t } from '../../languageHandler'; import { Droppable } from 'react-beautiful-dnd'; import classNames from 'classnames'; -const TagPanel = React.createClass({ +const TagPanel = createReactClass({ displayName: 'TagPanel', contextTypes: { diff --git a/src/components/structures/TagPanelButtons.js b/src/components/structures/TagPanelButtons.js index bbd9d28576..a850e8c34c 100644 --- a/src/components/structures/TagPanelButtons.js +++ b/src/components/structures/TagPanelButtons.js @@ -15,12 +15,13 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../index'; import dis from '../../dispatcher'; import Modal from '../../Modal'; import { _t } from '../../languageHandler'; -const TagPanelButtons = React.createClass({ +const TagPanelButtons = createReactClass({ displayName: 'TagPanelButtons', diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 5c18267637..cdeea78204 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -19,8 +19,9 @@ limitations under the License. import SettingsStore from "../../settings/SettingsStore"; -const React = require('react'); -const ReactDOM = require("react-dom"); +import React from 'react'; +import createReactClass from 'create-react-class'; +import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; import Promise from 'bluebird'; @@ -58,7 +59,7 @@ if (DEBUG) { * * Also responsible for handling and sending read receipts. */ -const TimelinePanel = React.createClass({ +const TimelinePanel = createReactClass({ displayName: 'TimelinePanel', propTypes: { diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 6f26f0af35..da0ca7fe99 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -14,14 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -const React = require('react'); +import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import ContentMessages from '../../ContentMessages'; const dis = require('../../dispatcher'); const filesize = require('filesize'); import { _t } from '../../languageHandler'; -module.exports = React.createClass({displayName: 'UploadBar', +module.exports = createReactClass({ + displayName: 'UploadBar', propTypes: { room: PropTypes.object, }, diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index fd35fdbeef..ef4ede517a 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -16,13 +16,14 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import SyntaxHighlight from '../views/elements/SyntaxHighlight'; import {_t} from "../../languageHandler"; import sdk from "../../index"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ViewSource', propTypes: { From 42ba5f6f0a39562ac242198f952f457159cd2df1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 30 Aug 2019 11:25:17 +0200 Subject: [PATCH 283/413] force model update after composition finishes --- src/components/views/rooms/BasicMessageComposer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index d55e9acc86..770e4766f3 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -152,6 +152,9 @@ export default class BasicMessageEditor extends React.Component { _onCompositionEnd = (event) => { this._isIMEComposing = false; + // some browsers (chromium) don't fire an input event after ending a composition + // so trigger a model update after the composition is done by calling the input handler + this._onInput({inputType: "insertCompositionText"}); } _onInput = (event) => { From b16f983a1f509004d4238d632ee7444ef5c5120e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 30 Aug 2019 11:51:29 +0200 Subject: [PATCH 284/413] put display name in user pill text fallback instead of mxid --- src/editor/serialize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/serialize.js b/src/editor/serialize.js index 5a1a941309..f3371ac8ee 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -54,7 +54,7 @@ export function textSerialize(model) { return text + part.text; case "room-pill": case "user-pill": - return text + `${part.resourceId}`; + return text + `${part.text}`; } }, ""); } From 00a06af4194ad882e8fed8b9f2f622367350b3b2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Aug 2019 10:57:46 +0100 Subject: [PATCH 285/413] Hide the E2EE PL selector if room is already encrypted Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/tabs/room/RolesRoomSettingsTab.js | 5 +++++ src/i18n/strings/en_EN.json | 1 + 2 files changed, 6 insertions(+) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js index 8aabc8d340..6b5fded674 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js @@ -306,6 +306,11 @@ export default class RolesRoomSettingsTab extends React.Component {
; }); + // hide the power level selector for enabling E2EE if it the room is already encrypted + if (client.isRoomEncrypted(this.props.roomId)) { + delete eventsLevels["m.room.encryption"]; + } + const eventPowerSelectors = Object.keys(eventsLevels).map((eventType, i) => { let label = plEventsToLabels[eventType]; if (label) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ce4598ed5f..c28431bc26 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -681,6 +681,7 @@ "Change permissions": "Change permissions", "Change topic": "Change topic", "Upgrade the room": "Upgrade the room", + "Enable room encryption": "Enable room encryption", "Modify widgets": "Modify widgets", "Failed to unban": "Failed to unban", "Unban": "Unban", From 8ff0883c2263dd29adc1d5f342ea1815ede7ba84 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 30 Aug 2019 18:29:07 +0100 Subject: [PATCH 286/413] Add a dialog when inviting via slash command without IS This adds a dialog to ask how you want to proceed when trying to invite via email when there is no IS configured. Fixes https://github.com/vector-im/riot-web/issues/10619 --- src/SlashCommands.js | 46 +++++++++++++++++++++++++++++++++---- src/i18n/strings/en_EN.json | 3 +++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 5ed1adb40f..8000d9c2aa 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -31,6 +31,9 @@ import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import WidgetUtils from "./utils/WidgetUtils"; import {textToHtmlRainbow} from "./utils/colour"; import Promise from "bluebird"; +import { getAddressType } from './UserAddress'; +import { abbreviateUrl } from './utils/UrlUtils'; +import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; const singleMxcUpload = async () => { return new Promise((resolve) => { @@ -342,11 +345,46 @@ export const CommandMap = { if (matches) { // We use a MultiInviter to re-use the invite logic, even though // we're only inviting one user. - const userId = matches[1]; + const address = matches[1]; + // If we need an identity server but don't have one, things + // get a bit more complex here, but we try to show something + // meaningful. + let finished = Promise.resolve(); + if ( + getAddressType(address) === 'email' && + !MatrixClientPeg.get().getIdentityServerUrl() + ) { + const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); + if (defaultIdentityServerUrl) { + ({ finished } = Modal.createTrackedDialog('Slash Commands', 'Identity server', + QuestionDialog, { + title: _t("Use an identity server"), + description:

{_t( + "Use an identity server to invite by email. " + + "Click continue to use the default identity server " + + "(%(defaultIdentityServerName)s) or manage in Settings.", + { + defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), + }, + )}

, + button: _t("Continue"), + }, + )); + } else { + return reject(_t("Use an identity server to invite by email. Manage in Settings.")); + } + } const inviter = new MultiInviter(roomId); - return success(inviter.invite([userId]).then(() => { - if (inviter.getCompletionState(userId) !== "invited") { - throw new Error(inviter.getErrorText(userId)); + return success(finished.then(([useDefault] = []) => { + if (useDefault) { + useDefaultIdentityServer(); + } else if (useDefault === false) { + throw new Error(_t("Use an identity server to invite by email. Manage in Settings.")); + } + return inviter.invite([address]); + }).then(() => { + if (inviter.getCompletionState(address) !== "invited") { + throw new Error(inviter.getErrorText(address)); } })); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ce4598ed5f..3823b7eb08 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -161,6 +161,9 @@ "This room has no topic.": "This room has no topic.", "Sets the room name": "Sets the room name", "Invites user with given id to current room": "Invites user with given id to current room", + "Use an identity server": "Use an identity server", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.", + "Use an identity server to invite by email. Manage in Settings.": "Use an identity server to invite by email. Manage in Settings.", "Joins room with given alias": "Joins room with given alias", "Leave room": "Leave room", "Unrecognised room alias:": "Unrecognised room alias:", From d47fb799a59c01d564c61631828f09b29c28ec14 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 30 Aug 2019 15:50:51 -0600 Subject: [PATCH 287/413] Disable MSISDN registration if the homeserver doesn't support it --- src/components/views/auth/RegistrationForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index d3f275ffc3..68e0a4b99a 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -447,7 +447,7 @@ module.exports = React.createClass({ _showPhoneNumber() { const threePidLogin = !SdkConfig.get().disable_3pid_login; const haveIs = Boolean(this.props.serverConfig.isUrl); - if (!threePidLogin || !haveIs || !this._authStepIsUsed('m.login.msisdn')) { + if (!threePidLogin || (this.props.serverRequiresIdServer && !haveIs) || !this._authStepIsUsed('m.login.msisdn')) { return false; } return true; From 2bfffa76b57c993e491c199d97d7efabb486548c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 30 Aug 2019 15:54:00 -0600 Subject: [PATCH 288/413] Appease the linter --- src/components/views/auth/RegistrationForm.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 68e0a4b99a..a226d29e6a 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -447,7 +447,8 @@ module.exports = React.createClass({ _showPhoneNumber() { const threePidLogin = !SdkConfig.get().disable_3pid_login; const haveIs = Boolean(this.props.serverConfig.isUrl); - if (!threePidLogin || (this.props.serverRequiresIdServer && !haveIs) || !this._authStepIsUsed('m.login.msisdn')) { + const haveRequiredIs = this.props.serverRequiresIdServer && !haveIs; + if (!threePidLogin || haveRequiredIs || !this._authStepIsUsed('m.login.msisdn')) { return false; } return true; From cfff576ef42638436215f25a43cac80f341db857 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 1 Sep 2019 18:04:24 -0600 Subject: [PATCH 289/413] Add a button to MemberInfo to deactivate a user Part of https://github.com/vector-im/riot-web/issues/4125 --- src/components/views/rooms/MemberInfo.js | 50 +++++++++++++++++++++--- src/i18n/strings/en_EN.json | 3 ++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 629790a743..bb8fb7944f 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -46,6 +46,7 @@ import MultiInviter from "../../../utils/MultiInviter"; import SettingsStore from "../../../settings/SettingsStore"; import E2EIcon from "./E2EIcon"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import MatrixClientPeg from "../../../MatrixClientPeg"; module.exports = React.createClass({ displayName: 'MemberInfo', @@ -61,6 +62,7 @@ module.exports = React.createClass({ ban: false, mute: false, modifyLevel: false, + synapseDeactivate: false, }, muted: false, isTargetMod: false, @@ -215,8 +217,8 @@ module.exports = React.createClass({ } }, - _updateStateForNewMember: function(member) { - const newState = this._calculateOpsPermissions(member); + _updateStateForNewMember: async function(member) { + const newState = await this._calculateOpsPermissions(member); newState.devicesLoading = true; newState.devices = null; this.setState(newState); @@ -464,6 +466,25 @@ module.exports = React.createClass({ }); }, + onSynapseDeactivate: function() { + const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); + Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { + title: _t("Deactivate user?"), + description: +
{ _t( + "Deactivating this user will log them out and prevent them from logging back in. Additionally, " + + "they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to " + + "deactivate this user?" + ) }
, + button: _t("Deactivate user"), + danger: true, + onFinished: (accepted) => { + if (!accepted) return; + this.context.matrixClient.deactivateSynapseUser(this.props.member.userId); + }, + }); + }, + _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { this.setState({ updating: this.state.updating + 1 }); this.context.matrixClient.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( @@ -548,7 +569,7 @@ module.exports = React.createClass({ }); }, - _calculateOpsPermissions: function(member) { + _calculateOpsPermissions: async function(member) { const defaultPerms = { can: {}, muted: false, @@ -564,7 +585,7 @@ module.exports = React.createClass({ const them = member; return { - can: this._calculateCanPermissions( + can: await this._calculateCanPermissions( me, them, powerLevels.getContent(), ), muted: this._isMuted(them, powerLevels.getContent()), @@ -572,7 +593,7 @@ module.exports = React.createClass({ }; }, - _calculateCanPermissions: function(me, them, powerLevels) { + _calculateCanPermissions: async function(me, them, powerLevels) { const isMe = me.userId === them.userId; const can = { kick: false, @@ -581,6 +602,10 @@ module.exports = React.createClass({ modifyLevel: false, modifyLevelMax: 0, }; + + // Calculate permissions for Synapse before doing the PL checks + can.synapseDeactivate = await this.context.matrixClient.isSynapseAdministrator(); + const canAffectUser = them.powerLevel < me.powerLevel || isMe; if (!canAffectUser) { //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); @@ -786,6 +811,7 @@ module.exports = React.createClass({ let banButton; let muteButton; let giveModButton; + let synapseDeactivateButton; let spinner; if (this.props.member.userId !== this.context.matrixClient.credentials.userId) { @@ -893,8 +919,19 @@ module.exports = React.createClass({ ; } + // We don't need a perfect check here, just something to pass as "probably not our homeserver". If + // someone does figure out how to bypass this check the worst that happens is an error. + const sameHomeserver = this.props.member.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`); + if (this.state.can.synapseDeactivate && sameHomeserver) { + synapseDeactivateButton = ( + + {_t("Deactivate user")} + + ); + } + let adminTools; - if (kickButton || banButton || muteButton || giveModButton) { + if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton) { adminTools =

{ _t("Admin Tools") }

@@ -904,6 +941,7 @@ module.exports = React.createClass({ { kickButton } { banButton } { giveModButton } + { synapseDeactivateButton }
; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c28431bc26..1423795672 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -788,6 +788,9 @@ "Demote": "Demote", "Failed to mute user": "Failed to mute user", "Failed to toggle moderator status": "Failed to toggle moderator status", + "Deactivate user?": "Deactivate user?", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", + "Deactivate user": "Deactivate user", "Failed to change power level": "Failed to change power level", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", From a4376a76f06668e5293b350695546a7969b33b77 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 11:25:29 +0200 Subject: [PATCH 290/413] only pass keyboard to autocomplete when it has selections otherwise if tab is pressed, try to tab complete the last word --- src/components/views/rooms/BasicMessageComposer.js | 2 +- src/editor/autocomplete.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index d55e9acc86..ec1fd3d276 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -265,7 +265,7 @@ export default class BasicMessageEditor extends React.Component { handled = true; // autocomplete or enter to send below shouldn't have any modifier keys pressed. } else if (!(event.metaKey || event.altKey || event.shiftKey)) { - if (model.autoComplete) { + if (model.autoComplete && model.autoComplete.hasCompletions()) { const autoComplete = model.autoComplete; switch (event.key) { case "ArrowUp": diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index 79a69c07a6..a4cbc1cec8 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -41,6 +41,11 @@ export default class AutocompleteWrapperModel { return this._getAutocompleterComponent().hasSelection(); } + hasCompletions() { + const ac = this._getAutocompleterComponent(); + return ac && ac.countCompletions() > 0; + } + onEnter() { this._updateCallback({close: true}); } From 00d81eece924c95d9850a55c27cf5ecf6c6194d7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 11:26:20 +0200 Subject: [PATCH 291/413] don't accept @/#/: as part of command, allow to create pill candidate so if you type these 3 characters, you get the correct autocomplete --- src/editor/parts.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/editor/parts.js b/src/editor/parts.js index d14fcf98a2..9ca9205bcd 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -465,10 +465,6 @@ export class CommandPartCreator extends PartCreator { } class CommandPart extends PillCandidatePart { - acceptsInsertion(chr, i) { - return PlainPart.prototype.acceptsInsertion.call(this, chr, i); - } - get type() { return "command"; } From fdd23b34ae0cd11ab3c0831f69d5176f28cce40c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 11:27:22 +0200 Subject: [PATCH 292/413] also look backwards into commands for last word to tab-complete --- src/components/views/rooms/BasicMessageComposer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index ec1fd3d276..4d746448ef 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -306,7 +306,11 @@ export default class BasicMessageEditor extends React.Component { const position = model.positionForOffset(caret.offset, caret.atNodeEnd); const range = model.startRange(position); range.expandBackwardsWhile((index, offset, part) => { - return part.text[offset] !== " " && (part.type === "plain" || part.type === "pill-candidate"); + return part.text[offset] !== " " && ( + part.type === "plain" || + part.type === "pill-candidate" || + part.type === "command" + ); }); const {partCreator} = model; // await for auto-complete to be open From 3feeaceb684fe29c074be01072f7e1b6a2501900 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 2 Sep 2019 10:33:19 +0100 Subject: [PATCH 293/413] Restrict green link colours to address picker dialog This changes to a more targeted selection of what becomes green (just the actionable links in address picker). Fixes https://github.com/vector-im/riot-web/issues/10703 --- res/css/_common.scss | 6 ------ res/css/views/dialogs/_AddressPickerDialog.scss | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 8252d5930e..859c0006a1 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -281,12 +281,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { box-shadow: 2px 15px 30px 0 $dialog-shadow-color; border-radius: 4px; overflow-y: auto; - - a:link, - a:hover, - a:visited { - @mixin mx_Dialog_link; - } } .mx_Dialog_fixedWidth { diff --git a/res/css/views/dialogs/_AddressPickerDialog.scss b/res/css/views/dialogs/_AddressPickerDialog.scss index 168310507c..39a9260ba3 100644 --- a/res/css/views/dialogs/_AddressPickerDialog.scss +++ b/res/css/views/dialogs/_AddressPickerDialog.scss @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd 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,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_AddressPickerDialog { + a:link, + a:hover, + a:visited { + @mixin mx_Dialog_link; + } +} + /* Using a textarea for this element, to circumvent autofill */ .mx_AddressPickerDialog_input, .mx_AddressPickerDialog_input:focus { From b46e001d0ac0f87118c9e12d0659cafc5272e687 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 13:51:46 +0200 Subject: [PATCH 294/413] allow pill-candidate parts in commands --- src/components/views/rooms/SendMessageComposer.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 0e03d83467..8dbbb6eca2 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -163,10 +163,7 @@ export default class SendMessageComposer extends React.Component { _isSlashCommand() { const parts = this.model.parts; - const isPlain = parts.reduce((isPlain, part) => { - return isPlain && (part.type === "command" || part.type === "plain" || part.type === "newline"); - }, true); - return isPlain && parts.length > 0 && parts[0].text.startsWith("/"); + return parts.length && parts[0].type === "command"; } async _runSlashCommand() { From c5953718452ee054a07933b73ccf8eca14a3c5d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 14:06:30 +0200 Subject: [PATCH 295/413] share user pill postfix between autocomplete and insert mention where we decide to add a colon only if the composer is empty --- src/components/views/rooms/SendMessageComposer.js | 5 +++-- src/editor/autocomplete.js | 4 +--- src/editor/parts.js | 6 ++++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 0e03d83467..b6d9c42707 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -290,11 +290,12 @@ export default class SendMessageComposer extends React.Component { const member = this.props.room.getMember(userId); const displayName = member ? member.rawDisplayName : userId; - const userPillPart = partCreator.userPill(displayName, userId); const caret = this._editorRef.getCaret(); const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + const insertIndex = position.index + 1; + const parts = partCreator.createMentionParts(insertIndex, displayName, userId); model.transform(() => { - const addedLen = model.insert([userPillPart], position); + const addedLen = model.insert(parts, position); return model.positionForOffset(caret.offset + addedLen, true); }); // refocus on composer, as we just clicked "Mention" diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index fe6f3bfe1f..beb155f297 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -106,9 +106,7 @@ export default class AutocompleteWrapperModel { if (completionId === "@room") { return [this._partCreator.atRoomPill(completionId)]; } else { - const pill = this._partCreator.userPill(text, completionId); - const postfix = this._partCreator.plain(this._partIndex === 0 ? ": " : " "); - return [pill, postfix]; + return this._partCreator.createMentionParts(this._partIndex, text, completionId); } } case "#": diff --git a/src/editor/parts.js b/src/editor/parts.js index 9ca9205bcd..c10392bfd2 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -441,6 +441,12 @@ export class PartCreator { const member = this._room.getMember(userId); return new UserPillPart(userId, displayName, member); } + + createMentionParts(partIndex, displayName, userId) { + const pill = this.userPill(displayName, userId); + const postfix = this.plain(partIndex === 0 ? ": " : " "); + return [pill, postfix]; + } } // part creator that support auto complete for /commands, From 2683627a827c9e1b8c10f699fa57b2c7be2729ff Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 14:26:15 +0200 Subject: [PATCH 296/413] disable spell check for pills in the new composer --- src/editor/parts.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor/parts.js b/src/editor/parts.js index 9ca9205bcd..07062a9ed1 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -164,6 +164,7 @@ class PillPart extends BasePart { toDOMNode() { const container = document.createElement("span"); + container.setAttribute("spellcheck", "false"); container.className = this.className; container.appendChild(document.createTextNode(this.text)); this.setAvatar(container); From b9cb22e153430bbdbac0e2c36eb5985c475e9494 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 14:36:31 +0200 Subject: [PATCH 297/413] dont allow sending empty messages --- src/components/views/rooms/SendMessageComposer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 0e03d83467..ca7b94999d 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -203,6 +203,9 @@ export default class SendMessageComposer extends React.Component { } _sendMessage() { + if (this.model.isEmpty) { + return; + } if (!containsEmote(this.model) && this._isSlashCommand()) { this._runSlashCommand(); } else { From b7768f34f2bad7cd890d32cebc41f2934575c5a2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 30 Aug 2019 18:09:23 +0200 Subject: [PATCH 298/413] Add legend and style it --- res/css/views/rooms/_SendMessageComposer.scss | 30 +++++++++++++++++-- .../views/rooms/SendMessageComposer.js | 1 + 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index d20f7107b3..d6da1b87fa 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -30,7 +30,7 @@ limitations under the License. flex-direction: column; // min-height at this level so the mx_BasicMessageComposer_input // still stays vertically centered when less than 50px - min-height: 50px; + min-height: 42px; .mx_BasicMessageComposer_input { padding: 3px 0; @@ -38,7 +38,7 @@ limitations under the License. // in it's parent vertically // while keeping the autocomplete at the top // of the composer. The parent needs to be a flex container for this to work. - margin: auto 0; + margin: auto 0 0 0; // max-height at this level so autocomplete doesn't get scrolled too max-height: 140px; overflow-y: auto; @@ -49,5 +49,31 @@ limitations under the License. position: relative; height: 0; } + + .mx_SendMessageComposer_legend { + height: 16px; + box-sizing: content-box; + font-size: 8px; + line-height: 10px; + padding: 0 0 2px 0; + color: $light-fg-color; + user-select: none; + visibility: hidden; + + * { + display: inline-block; + margin: 0 10px 0 0; + padding: 1px; + } + + code { + border-radius: 2px; + background-color: $focus-bg-color; + } + + &.mx_SendMessageComposer_legend_shown { + visibility: visible; + } + } } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 0e03d83467..65e6673124 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -343,6 +343,7 @@ export default class SendMessageComposer extends React.Component { placeholder={this.props.placeholder} onChange={this._saveStoredEditorState} /> +
**bold**_italic_~strikethrough~`code`> quote
); } From f200327ef21ff494de737553ca4ded1bd4e4fad7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 15:59:17 +0200 Subject: [PATCH 299/413] show/hide legend when focusing/blurring --- .../views/rooms/BasicMessageComposer.js | 10 ++++++-- .../views/rooms/SendMessageComposer.js | 23 ++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index d55e9acc86..ef6fda6595 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -222,15 +222,21 @@ export default class BasicMessageEditor extends React.Component { return this.getCaret().offset === this._lastTextLength; } - _onBlur = () => { + _onBlur = (event) => { document.removeEventListener("selectionchange", this._onSelectionChange); + if (this.props.onBlur) { + this.props.onBlur(event); + } } - _onFocus = () => { + _onFocus = (event) => { document.addEventListener("selectionchange", this._onSelectionChange); // force to recalculate this._lastSelection = null; this._refreshLastCaretIfNeeded(); + if (this.props.onFocus) { + this.props.onFocus(event); + } } _onSelectionChange = () => { diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 65e6673124..8772bf3b66 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -33,6 +33,7 @@ import sdk from '../../../index'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; +import classNames from "classnames"; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -87,6 +88,7 @@ export default class SendMessageComposer extends React.Component { constructor(props, context) { super(props, context); + this.state = {}; this.model = null; this._editorRef = null; this.currentlyComposedEditorState = null; @@ -329,7 +331,18 @@ export default class SendMessageComposer extends React.Component { } } + _onFocus = () => { + this.setState({focused: true}); + } + + _onBlur = () => { + this.setState({focused: false}); + } + render() { + const legendClasses = classNames("mx_SendMessageComposer_legend", { + "mx_SendMessageComposer_legend_shown": this.state.focused, + }); return (
@@ -342,8 +355,16 @@ export default class SendMessageComposer extends React.Component { label={this.props.placeholder} placeholder={this.props.placeholder} onChange={this._saveStoredEditorState} + onFocus={this._onFocus} + onBlur={this._onBlur} /> -
**bold**_italic_~strikethrough~`code`> quote
+
+ **bold** + _italic_ + <del>strikethrough</del> + `code` + > quote +
); } From 5b54cf566de3e38d5d2c7d43b9d667aa0ff6d483 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 16:23:56 +0200 Subject: [PATCH 300/413] deserialize headers from html back to markdown --- src/editor/deserialize.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index d59e4ca123..08c66f592a 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -71,8 +71,20 @@ function parseCodeBlock(n, partCreator) { return parts; } +function parseHeader(el, partCreator) { + const depth = parseInt(el.nodeName.substr(1), 10); + return partCreator.plain("#".repeat(depth) + " "); +} + function parseElement(n, partCreator, state) { switch (n.nodeName) { + case "H1": + case "H2": + case "H3": + case "H4": + case "H5": + case "H6": + return parseHeader(n, partCreator); case "A": return parseLink(n, partCreator); case "BR": From 6163cefa6ae12632c5b81f6f4ca51e439843eebf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 16:29:25 +0200 Subject: [PATCH 301/413] set dirty flag when programatically inserting text like with newlines --- src/components/views/rooms/BasicMessageComposer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 95855e8c81..c5661e561c 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -175,6 +175,7 @@ export default class BasicMessageEditor extends React.Component { const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset); caret.offset += textToInsert.length; this.props.model.update(newText, inputType, caret); + this._modifiedFlag = true; } // this is used later to see if we need to recalculate the caret From 00a69b996dd90b85fd58f18dc737128090929eff Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 2 Sep 2019 15:53:13 +0100 Subject: [PATCH 302/413] Clarify invite error text This fixes a typo in the message (valide) and also has better handling of error codes, because in some cases, we don't get one. Fixes https://github.com/vector-im/riot-web/issues/10683 --- src/components/views/rooms/RoomPreviewBar.js | 6 ++++-- src/i18n/strings/en_EN.json | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 7715bd9339..f2573d46b8 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -337,8 +337,10 @@ module.exports = React.createClass({ title = _t("Something went wrong with your invite to %(roomName)s", {roomName: this._roomName()}); const joinRule = this._joinRule(); - const errCodeMessage = _t("%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.", - {errcode: this.state.threePidFetchError.errcode}, + const errCodeMessage = _t( + "An error (%(errcode)s) was returned while trying to validate your " + + "invite. You could try to pass this information on to a room admin.", + {errcode: this.state.threePidFetchError.errcode || _t("unknown error code")}, ); switch (joinRule) { case "invite": diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d88484c283..8bfc918ada 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -892,7 +892,8 @@ "Re-join": "Re-join", "You were banned from %(roomName)s by %(memberName)s": "You were banned from %(roomName)s by %(memberName)s", "Something went wrong with your invite to %(roomName)s": "Something went wrong with your invite to %(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 was returned while trying to valide your invite. You could try to pass this information on to a room admin.", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.", + "unknown error code": "unknown error code", "You can only join it with a working invite.": "You can only join it with a working invite.", "You can still join it because this is a public room.": "You can still join it because this is a public room.", "Join the discussion": "Join the discussion", @@ -1399,7 +1400,6 @@ "Collapse Reply Thread": "Collapse Reply Thread", "End-to-end encryption information": "End-to-end encryption information", "Failed to set Direct Message status of room": "Failed to set Direct Message status of room", - "unknown error code": "unknown error code", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", "All messages (noisy)": "All messages (noisy)", "All messages": "All messages", From 41ca54bb0920a93cc0ae15ed063d5aedc8616f8b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 17:44:31 +0200 Subject: [PATCH 303/413] /plain command to bypass markdown conversion --- src/SlashCommands.js | 10 +++++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 8000d9c2aa..2d5617f8f0 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -118,7 +118,15 @@ export const CommandMap = { }, category: CommandCategories.messages, }), - + plain: new Command({ + name: 'plain', + args: '', + description: _td('Sends a message as plain text, without interpreting it as markdown'), + runFn: function(roomId, messages) { + return success(MatrixClientPeg.get().sendTextMessage(roomId, messages)); + }, + category: CommandCategories.messages, + }), ddg: new Command({ name: 'ddg', args: '', diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d88484c283..65b5abd034 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -141,6 +141,7 @@ "Other": "Other", "Usage": "Usage", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message", + "Sends a message as plain text, without interpreting it as markdown": "Sends a message as plain text, without interpreting it as markdown", "Searches DuckDuckGo for results": "Searches DuckDuckGo for results", "/ddg is not a command": "/ddg is not a command", "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", From 712c3e5450ef77bde96ee8730efc2c0557ae9d5c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 17:53:14 +0200 Subject: [PATCH 304/413] allow escaping the first slash to not write a command --- src/components/views/rooms/SendMessageComposer.js | 3 ++- src/editor/serialize.js | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 0e03d83467..f1fac1cab0 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -18,7 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; -import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; +import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand, unescapeMessage} from '../../../editor/serialize'; import {CommandPartCreator} from '../../../editor/parts'; import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; @@ -54,6 +54,7 @@ function createMessageContent(model, permalinkCreator) { if (isEmote) { model = stripEmoteCommand(model); } + model = unescapeMessage(model); const repliedToEvent = RoomViewStore.getQuotingEvent(); const body = textSerialize(model); diff --git a/src/editor/serialize.js b/src/editor/serialize.js index f3371ac8ee..07a1ad908e 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -74,3 +74,16 @@ export function stripEmoteCommand(model) { model.removeText({index: 0, offset: 0}, 4); return model; } + +export function unescapeMessage(model) { + const {parts} = model; + if (parts.length) { + const firstPart = parts[0]; + // only unescape \/ to / at start of editor + if (firstPart.type === "plain" && firstPart.text.startsWith("\\/")) { + model = model.clone(); + model.removeText({index: 0, offset: 0}, 1); + } + } + return model; +} From 06ae0645c77ae840c154d1e29491b758d428bf3c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 2 Sep 2019 17:56:16 +0200 Subject: [PATCH 305/413] fix lint --- src/components/views/rooms/SendMessageComposer.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index f1fac1cab0..b0a20a115a 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -18,7 +18,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; -import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand, unescapeMessage} from '../../../editor/serialize'; +import { + htmlSerializeIfNeeded, + textSerialize, + containsEmote, + stripEmoteCommand, + unescapeMessage, +} from '../../../editor/serialize'; import {CommandPartCreator} from '../../../editor/parts'; import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; From 1063da0ed1c82e9bf9892d8e2f1b966f6fb2070c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 3 Sep 2019 08:27:45 +0000 Subject: [PATCH 306/413] Revert "New composer: show markdown legend on focus" --- res/css/views/rooms/_SendMessageComposer.scss | 30 ++----------------- .../views/rooms/BasicMessageComposer.js | 10 ++----- .../views/rooms/SendMessageComposer.js | 22 -------------- 3 files changed, 4 insertions(+), 58 deletions(-) diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index d6da1b87fa..d20f7107b3 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -30,7 +30,7 @@ limitations under the License. flex-direction: column; // min-height at this level so the mx_BasicMessageComposer_input // still stays vertically centered when less than 50px - min-height: 42px; + min-height: 50px; .mx_BasicMessageComposer_input { padding: 3px 0; @@ -38,7 +38,7 @@ limitations under the License. // in it's parent vertically // while keeping the autocomplete at the top // of the composer. The parent needs to be a flex container for this to work. - margin: auto 0 0 0; + margin: auto 0; // max-height at this level so autocomplete doesn't get scrolled too max-height: 140px; overflow-y: auto; @@ -49,31 +49,5 @@ limitations under the License. position: relative; height: 0; } - - .mx_SendMessageComposer_legend { - height: 16px; - box-sizing: content-box; - font-size: 8px; - line-height: 10px; - padding: 0 0 2px 0; - color: $light-fg-color; - user-select: none; - visibility: hidden; - - * { - display: inline-block; - margin: 0 10px 0 0; - padding: 1px; - } - - code { - border-radius: 2px; - background-color: $focus-bg-color; - } - - &.mx_SendMessageComposer_legend_shown { - visibility: visible; - } - } } diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 9256bb9a0e..c5661e561c 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -226,21 +226,15 @@ export default class BasicMessageEditor extends React.Component { return this.getCaret().offset === this._lastTextLength; } - _onBlur = (event) => { + _onBlur = () => { document.removeEventListener("selectionchange", this._onSelectionChange); - if (this.props.onBlur) { - this.props.onBlur(event); - } } - _onFocus = (event) => { + _onFocus = () => { document.addEventListener("selectionchange", this._onSelectionChange); // force to recalculate this._lastSelection = null; this._refreshLastCaretIfNeeded(); - if (this.props.onFocus) { - this.props.onFocus(event); - } } _onSelectionChange = () => { diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 4bb311dfd1..666d2da971 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -33,7 +33,6 @@ import sdk from '../../../index'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; -import classNames from "classnames"; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -88,7 +87,6 @@ export default class SendMessageComposer extends React.Component { constructor(props, context) { super(props, context); - this.state = {}; this.model = null; this._editorRef = null; this.currentlyComposedEditorState = null; @@ -332,18 +330,7 @@ export default class SendMessageComposer extends React.Component { } } - _onFocus = () => { - this.setState({focused: true}); - } - - _onBlur = () => { - this.setState({focused: false}); - } - render() { - const legendClasses = classNames("mx_SendMessageComposer_legend", { - "mx_SendMessageComposer_legend_shown": this.state.focused, - }); return (
@@ -356,16 +343,7 @@ export default class SendMessageComposer extends React.Component { label={this.props.placeholder} placeholder={this.props.placeholder} onChange={this._saveStoredEditorState} - onFocus={this._onFocus} - onBlur={this._onBlur} /> -
- **bold** - _italic_ - <del>strikethrough</del> - `code` - > quote -
); } From 648ae37ff48de15eb4073befcc155a3b201bf169 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 3 Sep 2019 15:58:05 +0200 Subject: [PATCH 307/413] make DocumentOffset compatible with what is returned from dom/getCaret so we can return a DocumentOffset from there without breakage --- src/editor/offset.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editor/offset.js b/src/editor/offset.js index 7054836bdc..c638640f6f 100644 --- a/src/editor/offset.js +++ b/src/editor/offset.js @@ -15,12 +15,12 @@ limitations under the License. */ export default class DocumentOffset { - constructor(offset, atEnd) { + constructor(offset, atNodeEnd) { this.offset = offset; - this.atEnd = atEnd; + this.atNodeEnd = atNodeEnd; } asPosition(model) { - return model.positionForOffset(this.offset, this.atEnd); + return model.positionForOffset(this.offset, this.atNodeEnd); } } From eb87301855347e76a1244135895c25778e30cce2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 3 Sep 2019 15:58:50 +0200 Subject: [PATCH 308/413] allow getting the DocumentOffset for any node+offset, not just focusNode we need this to get both offsets of the selection boundaries getSelectionOffsetAndText offers the extra flexibility, getCaretOffsetAndText keeps the old api for focusNode/focusOffset Also did some renaming here now that it's not just for the caret anymore --- src/editor/dom.js | 51 ++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/editor/dom.js b/src/editor/dom.js index 4f15a57303..45e30d421a 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -16,6 +16,7 @@ limitations under the License. */ import {CARET_NODE_CHAR, isCaretNode} from "./render"; +import DocumentOffset from "./offset"; export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) { let node = rootNode.firstChild; @@ -40,26 +41,30 @@ export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback } export function getCaretOffsetAndText(editor, sel) { - let {focusNode, focusOffset} = sel; - // sometimes focusNode is an element, and then focusOffset means + const {offset, text} = getSelectionOffsetAndText(editor, sel.focusNode, sel.focusOffset); + return {caret: offset, text}; +} + +export function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { + // sometimes selectionNode is an element, and then selectionOffset means // the index of a child element ... - 1 🤷 - if (focusNode.nodeType === Node.ELEMENT_NODE && focusOffset !== 0) { - focusNode = focusNode.childNodes[focusOffset - 1]; - focusOffset = focusNode.textContent.length; + if (selectionNode.nodeType === Node.ELEMENT_NODE && selectionOffset !== 0) { + selectionNode = selectionNode.childNodes[selectionOffset - 1]; + selectionOffset = selectionNode.textContent.length; } - const {text, focusNodeOffset} = getTextAndFocusNodeOffset(editor, focusNode, focusOffset); - const caret = getCaret(focusNode, focusNodeOffset, focusOffset); - return {caret, text}; + const {text, offsetToNode} = getTextAndOffsetToNode(editor, selectionNode); + const offset = getCaret(selectionNode, offsetToNode, selectionOffset); + return {offset, text}; } // gets the caret position details, ignoring and adjusting to // the ZWS if you're typing in a caret node -function getCaret(focusNode, focusNodeOffset, focusOffset) { - let atNodeEnd = focusOffset === focusNode.textContent.length; - if (focusNode.nodeType === Node.TEXT_NODE && isCaretNode(focusNode.parentElement)) { - const zwsIdx = focusNode.nodeValue.indexOf(CARET_NODE_CHAR); - if (zwsIdx !== -1 && zwsIdx < focusOffset) { - focusOffset -= 1; +function getCaret(node, offsetToNode, offsetWithinNode) { + let atNodeEnd = offsetWithinNode === node.textContent.length; + if (node.nodeType === Node.TEXT_NODE && isCaretNode(node.parentElement)) { + const zwsIdx = node.nodeValue.indexOf(CARET_NODE_CHAR); + if (zwsIdx !== -1 && zwsIdx < offsetWithinNode) { + offsetWithinNode -= 1; } // if typing in a caret node, you're either typing before or after the ZWS. // In both cases, you should be considered at node end because the ZWS is @@ -67,20 +72,20 @@ function getCaret(focusNode, focusNodeOffset, focusOffset) { // that caret node will be removed. atNodeEnd = true; } - return {offset: focusNodeOffset + focusOffset, atNodeEnd}; + return new DocumentOffset(offsetToNode + offsetWithinNode, atNodeEnd); } // gets the text of the editor as a string, -// and the offset in characters where the focusNode starts in that string +// and the offset in characters where the selectionNode starts in that string // all ZWS from caret nodes are filtered out -function getTextAndFocusNodeOffset(editor, focusNode, focusOffset) { - let focusNodeOffset = 0; +function getTextAndOffsetToNode(editor, selectionNode) { + let offsetToNode = 0; let foundCaret = false; let text = ""; function enterNodeCallback(node) { if (!foundCaret) { - if (node === focusNode) { + if (node === selectionNode) { foundCaret = true; } } @@ -90,12 +95,12 @@ function getTextAndFocusNodeOffset(editor, focusNode, focusOffset) { // are not the last element in the DIV. if (node.tagName === "BR" && node.nextSibling) { text += "\n"; - focusNodeOffset += 1; + offsetToNode += 1; } const nodeText = node.nodeType === Node.TEXT_NODE && getTextNodeValue(node); if (nodeText) { if (!foundCaret) { - focusNodeOffset += nodeText.length; + offsetToNode += nodeText.length; } text += nodeText; } @@ -110,14 +115,14 @@ function getTextAndFocusNodeOffset(editor, focusNode, focusOffset) { if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") { text += "\n"; if (!foundCaret) { - focusNodeOffset += 1; + offsetToNode += 1; } } } walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback); - return {text, focusNodeOffset}; + return {text, offsetToNode}; } // get text value of text node, ignoring ZWS if it's a caret node From 0d02ab59d68c1700ca78326683daf98c2a062d4e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 3 Sep 2019 16:00:50 +0200 Subject: [PATCH 309/413] allow starting a range with both positions known already we'll need this to start a range for the selection --- src/editor/model.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index 613be5b4bd..75ab1d7706 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -394,11 +394,12 @@ export default class EditorModel { /** * Starts a range, which can span across multiple parts, to find and replace text. - * @param {DocumentPosition} position where to start the range + * @param {DocumentPosition} positionA a boundary of the range + * @param {DocumentPosition?} positionB the other boundary of the range, optional * @return {Range} */ - startRange(position) { - return new Range(this, position); + startRange(positionA, positionB = positionA) { + return new Range(this, positionA, positionB); } //mostly internal, called from Range.replace From 2e3e2ec420d73c45a832a6ce1da78bb68a0d5a69 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 3 Sep 2019 08:36:24 -0600 Subject: [PATCH 310/413] Fix member power levels in room settings Fixes https://github.com/vector-im/riot-web/issues/10736 We didn't have an onChange property on the PowerSelector component --- .../tabs/room/RolesRoomSettingsTab.js | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js index 6b5fded674..f76bd6efa2 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js @@ -151,6 +151,22 @@ export default class RolesRoomSettingsTab extends React.Component { client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent); }; + _onUserPowerLevelChanged = (value, powerLevelKey) => { + const client = MatrixClientPeg.get(); + const room = client.getRoom(this.props.roomId); + const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); + let plContent = plEvent ? (plEvent.getContent() || {}) : {}; + + // Clone the power levels just in case + plContent = Object.assign({}, plContent); + + // powerLevelKey should be a user ID + if (!plContent['users']) plContent['users'] = {}; + plContent['users'][powerLevelKey] = value; + + client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent); + }; + render() { const PowerSelector = sdk.getComponent('elements.PowerSelector'); @@ -220,15 +236,29 @@ export default class RolesRoomSettingsTab extends React.Component { const privilegedUsers = []; const mutedUsers = []; - Object.keys(userLevels).forEach(function(user) { + Object.keys(userLevels).forEach((user) => { const canChange = userLevels[user] < currentUserLevel && canChangeLevels; if (userLevels[user] > defaultUserLevel) { // privileged privilegedUsers.push( - , + , ); } else if (userLevels[user] < defaultUserLevel) { // muted mutedUsers.push( - , + , ); } }); From 67299842e3b921028d65afd2c456ed62e72e8d8e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 3 Sep 2019 15:32:15 +0100 Subject: [PATCH 311/413] Use more specific messaging for email invite preview This changes email invite previews to check more specific cases about whether the email has been added to your account, you have an IS, the email is bound, etc. In addition, it always allows you to join if you want to. Fixes https://github.com/vector-im/riot-web/issues/10669 --- src/components/views/rooms/RoomPreviewBar.js | 91 +++++++++++++++----- src/i18n/strings/en_EN.json | 9 +- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index f2573d46b8..05cd44156f 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -35,6 +35,8 @@ const MessageCase = Object.freeze({ Kicked: "Kicked", Banned: "Banned", OtherThreePIDError: "OtherThreePIDError", + InvitedEmailNotFoundInAccount: "InvitedEmailNotFoundInAccount", + InvitedEmailNoIdentityServer: "InvitedEmailNoIdentityServer", InvitedEmailMismatch: "InvitedEmailMismatch", Invite: "Invite", ViewingRoom: "ViewingRoom", @@ -106,12 +108,24 @@ module.exports = React.createClass({ }, _checkInvitedEmail: async function() { - // If this is an invite and we've been told what email - // address was invited, fetch the user's list of Threepids - // so we can check them against the one that was invited + // If this is an invite and we've been told what email address was + // invited, fetch the user's account emails and discovery bindings so we + // can check them against the email that was invited. if (this.props.inviterName && this.props.invitedEmail) { this.setState({busy: true}); try { + // Gather the account 3PIDs + const account3pids = await MatrixClientPeg.get().getThreePids(); + this.setState({ + accountEmails: account3pids.threepids.filter(b => b.medium === 'email') + .map(b => b.address), + }); + // If we have an IS connected, use that to lookup the email and + // check the bound MXID. + if (!MatrixClientPeg.get().getIdentityServerUrl()) { + this.setState({busy: false}); + return; + } const authClient = new IdentityAuthClient(); const identityAccessToken = await authClient.getAccessToken(); const result = await MatrixClientPeg.get().lookupThreePid( @@ -157,6 +171,13 @@ module.exports = React.createClass({ if (this.props.invitedEmail) { if (this.state.threePidFetchError) { return MessageCase.OtherThreePIDError; + } else if ( + this.state.accountEmails && + !this.state.accountEmails.includes(this.props.invitedEmail) + ) { + return MessageCase.InvitedEmailNotFoundInAccount; + } else if (!MatrixClientPeg.get().getIdentityServerUrl()) { + return MessageCase.InvitedEmailNoIdentityServer; } else if (this.state.invitedEmailMxid != MatrixClientPeg.get().getUserId()) { return MessageCase.InvitedEmailMismatch; } @@ -348,6 +369,8 @@ module.exports = React.createClass({ _t("You can only join it with a working invite."), errCodeMessage, ]; + primaryActionLabel = _t("Try to join anyway"); + primaryActionHandler = this.props.onJoinClick; break; case "public": subTitle = _t("You can still join it because this is a public room."); @@ -362,25 +385,51 @@ module.exports = React.createClass({ } break; } + case MessageCase.InvitedEmailNotFoundInAccount: { + title = _t( + "This invite to %(roomName)s was sent to %(email)s which is not " + + "associated with your account", + { + roomName: this._roomName(), + email: this.props.invitedEmail, + }, + ); + subTitle = _t( + "Link this email with your account in Settings to receive invites " + + "directly in Riot.", + ); + primaryActionLabel = _t("Join the discussion"); + primaryActionHandler = this.props.onJoinClick; + break; + } + case MessageCase.InvitedEmailNoIdentityServer: { + title = _t( + "This invite to %(roomName)s was sent to %(email)s", + { + roomName: this._roomName(), + email: this.props.invitedEmail, + }, + ); + subTitle = _t( + "Use an identity server in Settings to receive invites directly in Riot.", + ); + primaryActionLabel = _t("Join the discussion"); + primaryActionHandler = this.props.onJoinClick; + break; + } case MessageCase.InvitedEmailMismatch: { - title = _t("This invite to %(roomName)s wasn't sent to your account", - {roomName: this._roomName()}); - const joinRule = this._joinRule(); - if (joinRule === "public") { - subTitle = _t("You can still join it because this is a public room."); - primaryActionLabel = _t("Join the discussion"); - primaryActionHandler = this.props.onJoinClick; - } else { - subTitle = _t( - "Sign in with a different account, ask for another invite, or " + - "add the e-mail address %(email)s to this account.", - {email: this.props.invitedEmail}, - ); - if (joinRule !== "invite") { - primaryActionLabel = _t("Try to join anyway"); - primaryActionHandler = this.props.onJoinClick; - } - } + title = _t( + "This invite to %(roomName)s was sent to %(email)s", + { + roomName: this._roomName(), + email: this.props.invitedEmail, + }, + ); + subTitle = _t( + "Share this email in Settings to receive invites directly in Riot.", + ); + primaryActionLabel = _t("Join the discussion"); + primaryActionHandler = this.props.onJoinClick; break; } case MessageCase.Invite: { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8bfc918ada..8e74402277 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -895,11 +895,14 @@ "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.", "unknown error code": "unknown error code", "You can only join it with a working invite.": "You can only join it with a working invite.", + "Try to join anyway": "Try to join anyway", "You can still join it because this is a public room.": "You can still join it because this is a public room.", "Join the discussion": "Join the discussion", - "Try to join anyway": "Try to join anyway", - "This invite to %(roomName)s wasn't sent to your account": "This invite to %(roomName)s wasn't sent to your account", - "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "This invite to %(roomName)s was sent to %(email)s which is not associated with your account", + "Link this email with your account in Settings to receive invites directly in Riot.": "Link this email with your account in Settings to receive invites directly in Riot.", + "This invite to %(roomName)s was sent to %(email)s": "This invite to %(roomName)s was sent to %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Use an identity server in Settings to receive invites directly in Riot.", + "Share this email in Settings to receive invites directly in Riot.": "Share this email in Settings to receive invites directly in Riot.", "Do you want to chat with %(user)s?": "Do you want to chat with %(user)s?", "Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?", " invited you": " invited you", From 261bdab156f30287138dd37ad1196f48fd59803b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 3 Sep 2019 16:55:17 +0100 Subject: [PATCH 312/413] Fix indent --- src/components/views/rooms/RoomPreviewBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 05cd44156f..ccd4559586 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -117,8 +117,8 @@ module.exports = React.createClass({ // Gather the account 3PIDs const account3pids = await MatrixClientPeg.get().getThreePids(); this.setState({ - accountEmails: account3pids.threepids.filter(b => b.medium === 'email') - .map(b => b.address), + accountEmails: account3pids.threepids + .filter(b => b.medium === 'email').map(b => b.address), }); // If we have an IS connected, use that to lookup the email and // check the bound MXID. From 65ddfc0a50a58af63dd139a7719e0ed505af4336 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 3 Sep 2019 16:02:37 +0200 Subject: [PATCH 313/413] show format bar when text is selected --- .../views/rooms/_BasicMessageComposer.scss | 68 +++++++++++++++++++ res/img/format/bold.svg | 3 + res/img/format/code.svg | 7 ++ res/img/format/italics.svg | 3 + res/img/format/quote.svg | 5 ++ res/img/format/strikethrough.svg | 6 ++ .../views/rooms/BasicMessageComposer.js | 58 ++++++++++++++++ 7 files changed, 150 insertions(+) create mode 100644 res/img/format/bold.svg create mode 100644 res/img/format/code.svg create mode 100644 res/img/format/italics.svg create mode 100644 res/img/format/quote.svg create mode 100644 res/img/format/strikethrough.svg diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index bce0ecf325..23d61f5218 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -16,6 +16,8 @@ limitations under the License. */ .mx_BasicMessageComposer { + position: relative; + .mx_BasicMessageComposer_inputEmpty > :first-child::before { content: var(--placeholder); opacity: 0.333; @@ -71,4 +73,70 @@ limitations under the License. position: relative; height: 0; } + + .mx_BasicMessageComposer_formatBar { + display: none; + background-color: red; + width: calc(26px * 4); + height: 24px; + position: absolute; + cursor: pointer; + border-radius: 4px; + background: $message-action-bar-bg-color; + + &.mx_BasicMessageComposer_formatBar_shown { + display: block; + } + + > * { + white-space: nowrap; + display: inline-block; + position: relative; + border: 1px solid $message-action-bar-border-color; + margin-left: -1px; + + &:hover { + border-color: $message-action-bar-hover-border-color; + } + } + + .mx_BasicMessageComposer_formatButton { + width: 27px; + height: 24px; + box-sizing: border-box; + } + + .mx_BasicMessageComposer_formatButton::after { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + mask-repeat: no-repeat; + mask-position: center; + background-color: $message-action-bar-fg-color; + } + + .mx_BasicMessageComposer_formatBold::after { + mask-image: url('$(res)/img/format/bold.svg'); + } + + .mx_BasicMessageComposer_formatItalic::after { + mask-image: url('$(res)/img/format/italics.svg'); + } + + .mx_BasicMessageComposer_formatStrikethrough::after { + mask-image: url('$(res)/img/format/strikethrough.svg'); + } + + .mx_BasicMessageComposer_formatQuote::after { + mask-image: url('$(res)/img/format/quote.svg'); + } + + .mx_BasicMessageComposer_formatCode::after { + mask-image: url('$(res)/img/format/code.svg'); + } + + } } diff --git a/res/img/format/bold.svg b/res/img/format/bold.svg new file mode 100644 index 0000000000..634d735031 --- /dev/null +++ b/res/img/format/bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/format/code.svg b/res/img/format/code.svg new file mode 100644 index 0000000000..0a29bcd7bd --- /dev/null +++ b/res/img/format/code.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/format/italics.svg b/res/img/format/italics.svg new file mode 100644 index 0000000000..841afadffd --- /dev/null +++ b/res/img/format/italics.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/format/quote.svg b/res/img/format/quote.svg new file mode 100644 index 0000000000..82d3403314 --- /dev/null +++ b/res/img/format/quote.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/format/strikethrough.svg b/res/img/format/strikethrough.svg new file mode 100644 index 0000000000..fc02b0aae2 --- /dev/null +++ b/res/img/format/strikethrough.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index c5661e561c..291c179e46 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -74,8 +74,10 @@ export default class BasicMessageEditor extends React.Component { }; this._editorRef = null; this._autocompleteRef = null; + this._formatBarRef = null; this._modifiedFlag = false; this._isIMEComposing = false; + this._hasTextSelected = false; } _replaceEmoticon = (caretPosition, inputType, diff) => { @@ -239,6 +241,36 @@ export default class BasicMessageEditor extends React.Component { _onSelectionChange = () => { this._refreshLastCaretIfNeeded(); + const selection = document.getSelection(); + if (this._hasTextSelected && selection.isCollapsed) { + this._hasTextSelected = false; + if (this._formatBarRef) { + this._formatBarRef.classList.remove("mx_BasicMessageComposer_formatBar_shown"); + } + } else if (!selection.isCollapsed) { + this._hasTextSelected = true; + if (this._formatBarRef) { + this._formatBarRef.classList.add("mx_BasicMessageComposer_formatBar_shown"); + const selectionRect = selection.getRangeAt(0).getBoundingClientRect(); + + let leftOffset = 0; + let node = this._formatBarRef; + while (node.offsetParent) { + node = node.offsetParent; + leftOffset += node.offsetLeft; + } + + let topOffset = 0; + node = this._formatBarRef; + while (node.offsetParent) { + node = node.offsetParent; + topOffset += node.offsetTop; + } + + this._formatBarRef.style.left = `${selectionRect.left - leftOffset}px`; + this._formatBarRef.style.top = `${selectionRect.top - topOffset - 16 - 12}px`; + } + } } _onKeyDown = (event) => { @@ -392,6 +424,25 @@ export default class BasicMessageEditor extends React.Component { return caretPosition; } + _formatBold = () => { + } + + _formatItalic = () => { + + } + + _formatStrikethrough = () => { + + } + + _formatQuote = () => { + + } + + _formatCodeBlock = () => { + + } + render() { let autoComplete; if (this.state.autoComplete) { @@ -413,6 +464,13 @@ export default class BasicMessageEditor extends React.Component { }); return (
{ autoComplete } +
this._formatBarRef = ref}> + + + + + +
Date: Tue, 3 Sep 2019 16:03:03 +0200 Subject: [PATCH 314/413] sort positions in Range constructor, so start always comes before end --- src/editor/range.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/editor/range.js b/src/editor/range.js index 1aaf480733..1eebfeeb91 100644 --- a/src/editor/range.js +++ b/src/editor/range.js @@ -15,10 +15,11 @@ limitations under the License. */ export default class Range { - constructor(model, startPosition, endPosition = startPosition) { + constructor(model, positionA, positionB = positionA) { this._model = model; - this._start = startPosition; - this._end = endPosition; + const bIsLarger = positionA.compare(positionB) < 0; + this._start = bIsLarger ? positionA : positionB; + this._end = bIsLarger ? positionB : positionA; } moveStart(delta) { From 77b14beb1f770ce973fd45b30d68c490b5bcd216 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 3 Sep 2019 16:03:29 +0200 Subject: [PATCH 315/413] add getter for intersecting parts of range, and total length we'll need this when replacing selection, to preserve newlines, etc ... in the selected range (e.g. we can't just use range.text). --- src/editor/range.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/editor/range.js b/src/editor/range.js index 1eebfeeb91..d04bbbbfb1 100644 --- a/src/editor/range.js +++ b/src/editor/range.js @@ -57,4 +57,27 @@ export default class Range { this._model.replaceRange(this._start, this._end, parts); return newLength - oldLength; } + + /** + * Returns a copy of the (partial) parts within the range. + * For partial parts, only the text is adjusted to the part that intersects with the range. + */ + get parts() { + const parts = []; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + const serializedPart = part.serialize(); + serializedPart.text = part.text.substring(startIdx, endIdx); + const newPart = this._model.partCreator.deserializePart(serializedPart); + parts.push(newPart); + }); + return parts; + } + + get length() { + let len = 0; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + len += endIdx - startIdx; + }); + return len; + } } From 7dc39baaf3f9bb50bb2919172abbae1ade158968 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 3 Sep 2019 16:04:47 +0200 Subject: [PATCH 316/413] implement bold support in format bar --- .../views/rooms/BasicMessageComposer.js | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 291c179e46..9ba3e603b9 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; import {setCaretPosition} from '../../../editor/caret'; -import {getCaretOffsetAndText} from '../../../editor/dom'; +import {getCaretOffsetAndText, getSelectionOffsetAndText} from '../../../editor/dom'; import Autocomplete from '../rooms/Autocomplete'; import {autoCompleteCreator} from '../../../editor/parts'; import {renderModel} from '../../../editor/render'; @@ -424,7 +424,45 @@ export default class BasicMessageEditor extends React.Component { return caretPosition; } + _replaceSelection(callback) { + const selection = document.getSelection(); + if (selection.isCollapsed) { + return; + } + const focusOffset = getSelectionOffsetAndText( + this._editorRef, + selection.focusNode, + selection.focusOffset, + ).offset; + const anchorOffset = getSelectionOffsetAndText( + this._editorRef, + selection.anchorNode, + selection.anchorOffset, + ).offset; + const {model} = this.props; + const focusPosition = focusOffset.asPosition(model); + const anchorPosition = anchorOffset.asPosition(model); + const range = model.startRange(focusPosition, anchorPosition); + const firstPosition = focusPosition.compare(anchorPosition) < 0 ? focusPosition : anchorPosition; + + model.transform(() => { + const oldLen = range.length; + const newParts = callback(range); + const addedLen = range.replace(newParts); + const lastOffset = firstPosition.asOffset(model); + lastOffset.offset += oldLen + addedLen; + return lastOffset.asPosition(model); + }); + } + _formatBold = () => { + const {partCreator} = this.props.model; + this._replaceSelection(range => { + const parts = range.parts; + parts.splice(0, 0, partCreator.plain("**")); + parts.push(partCreator.plain("**")); + return parts; + }); } _formatItalic = () => { From c15dfc3c05531abe18d7ff8aecfdd048c28f63bc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 16:38:42 +0200 Subject: [PATCH 317/413] make Range start and end public --- src/editor/range.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/editor/range.js b/src/editor/range.js index d04bbbbfb1..2163076515 100644 --- a/src/editor/range.js +++ b/src/editor/range.js @@ -80,4 +80,12 @@ export default class Range { }); return len; } + + get start() { + return this._start; + } + + get end() { + return this._end; + } } From e7db660820b57773464c68f7ada16d8a47b40a1d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 16:39:07 +0200 Subject: [PATCH 318/413] fixup: css, we have 5 buttons --- res/css/views/rooms/_BasicMessageComposer.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index 23d61f5218..72a10bf074 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -77,7 +77,7 @@ limitations under the License. .mx_BasicMessageComposer_formatBar { display: none; background-color: red; - width: calc(26px * 4); + width: calc(26px * 5); height: 24px; position: absolute; cursor: pointer; From d4c7992f5a499efff2374c8e26d4fb604a19eee3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 16:39:56 +0200 Subject: [PATCH 319/413] first impl of inline formatting --- src/components/views/rooms/BasicMessageComposer.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 9ba3e603b9..b578aa9e0c 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -455,22 +455,26 @@ export default class BasicMessageEditor extends React.Component { }); } - _formatBold = () => { + _wrapSelection(prefix, suffix = prefix) { const {partCreator} = this.props.model; this._replaceSelection(range => { const parts = range.parts; - parts.splice(0, 0, partCreator.plain("**")); - parts.push(partCreator.plain("**")); + parts.splice(0, 0, partCreator.plain(prefix)); + parts.push(partCreator.plain(suffix)); return parts; }); } - _formatItalic = () => { + _formatBold = () => { + this._wrapSelection("**"); + } + _formatItalic = () => { + this._wrapSelection("*"); } _formatStrikethrough = () => { - + this._wrapSelection("", ""); } _formatQuote = () => { From 7f501b2aefdf709adcc9cd783484496a97c282b2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 16:40:13 +0200 Subject: [PATCH 320/413] first impl of quote formatting --- .../views/rooms/BasicMessageComposer.js | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index b578aa9e0c..81f4dcc8fe 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -478,7 +478,30 @@ export default class BasicMessageEditor extends React.Component { } _formatQuote = () => { - + const {model} = this.props; + const {partCreator} = this.props.model; + this._replaceSelection(range => { + const parts = range.parts; + parts.splice(0, 0, partCreator.plain("> ")); + const startsWithPartial = range.start.offset !== 0; + const isFirstPart = range.start.index === 0; + const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline"; + // prepend a newline if there is more text before the range on this line + if (startsWithPartial || (!isFirstPart && !previousIsNewline)) { + parts.splice(0, 0, partCreator.newline()); + } + // start at position 1 to make sure we skip the potentially inserted newline above, + // as we already inserted a quote sign for it above + for (let i = 1; i < parts.length; ++i) { + const part = parts[i]; + if (part.type === "newline") { + parts.splice(i + 1, 0, partCreator.plain("> ")); + } + } + parts.push(partCreator.newline()); + parts.push(partCreator.newline()); + return parts; + }); } _formatCodeBlock = () => { From 47d8d86bbe224ecfc4a5bc1aaf87ff4ea6e108c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 16:40:23 +0200 Subject: [PATCH 321/413] whitespace (in model) --- src/editor/model.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/editor/model.js b/src/editor/model.js index 75ab1d7706..34a796f733 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -388,7 +388,6 @@ export default class EditorModel { currentOffset += partLen; return false; }); - return new DocumentPosition(index, totalOffset - currentOffset); } From b72d1a78eca2afedfd223acd88d61296742fcb97 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 12:37:27 +0200 Subject: [PATCH 322/413] move inline formatting code out of react component --- .../views/rooms/BasicMessageComposer.js | 25 ++++++------ src/editor/dom.js | 16 ++++++++ src/editor/operations.js | 38 +++++++++++++++++++ 3 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 src/editor/operations.js diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 81f4dcc8fe..39d24b1bab 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -21,7 +21,10 @@ import PropTypes from 'prop-types'; import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; import {setCaretPosition} from '../../../editor/caret'; -import {getCaretOffsetAndText, getSelectionOffsetAndText} from '../../../editor/dom'; +import { + formatInline, +} from '../../../editor/operations'; +import {getCaretOffsetAndText, getRangeForSelection, getSelectionOffsetAndText} from '../../../editor/dom'; import Autocomplete from '../rooms/Autocomplete'; import {autoCompleteCreator} from '../../../editor/parts'; import {renderModel} from '../../../editor/render'; @@ -455,26 +458,24 @@ export default class BasicMessageEditor extends React.Component { }); } - _wrapSelection(prefix, suffix = prefix) { - const {partCreator} = this.props.model; - this._replaceSelection(range => { - const parts = range.parts; - parts.splice(0, 0, partCreator.plain(prefix)); - parts.push(partCreator.plain(suffix)); - return parts; - }); + _wrapSelectionAsInline(prefix, suffix = prefix) { + const range = getRangeForSelection( + this._editorRef, + this.props.model, + document.getSelection()); + formatInline(range, prefix, suffix); } _formatBold = () => { - this._wrapSelection("**"); + this._wrapSelectionAsInline("**"); } _formatItalic = () => { - this._wrapSelection("*"); + this._wrapSelectionAsInline("*"); } _formatStrikethrough = () => { - this._wrapSelection("", ""); + this._wrapSelectionAsInline("", ""); } _formatQuote = () => { diff --git a/src/editor/dom.js b/src/editor/dom.js index 45e30d421a..03ee7f2cfc 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -142,3 +142,19 @@ function getTextNodeValue(node) { return nodeText; } } + +export function getRangeForSelection(editor, model, selection) { + const focusOffset = getSelectionOffsetAndText( + editor, + selection.focusNode, + selection.focusOffset, + ).offset; + const anchorOffset = getSelectionOffsetAndText( + editor, + selection.anchorNode, + selection.anchorOffset, + ).offset; + const focusPosition = focusOffset.asPosition(model); + const anchorPosition = anchorOffset.asPosition(model); + return model.startRange(focusPosition, anchorPosition); +} diff --git a/src/editor/operations.js b/src/editor/operations.js new file mode 100644 index 0000000000..4f0757948a --- /dev/null +++ b/src/editor/operations.js @@ -0,0 +1,38 @@ +/* +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. +*/ + +/** + * Some common queries and transformations on the editor model + */ + +export function replaceRangeAndExpandSelection(model, range, newParts) { + model.transform(() => { + const oldLen = range.length; + const addedLen = range.replace(newParts); + const firstOffset = range.start.asOffset(model); + const lastOffset = firstOffset.add(oldLen + addedLen); + return model.startRange(firstOffset.asPosition(model), lastOffset.asPosition(model)); + }); +} + +export function formatInline(range, prefix, suffix = prefix) { + const {model, parts} = range; + const {partCreator} = model; + parts.unshift(partCreator.plain(prefix)); + parts.push(partCreator.plain(suffix)); + replaceRangeAndExpandSelection(model, range, parts); +} From b35a3531bb57d89f8098314491f4f0a953204121 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 12:40:03 +0200 Subject: [PATCH 323/413] move quote formatting out of react component --- .../views/rooms/BasicMessageComposer.js | 64 +++---------------- src/editor/dom.js | 2 +- src/editor/offset.js | 4 ++ src/editor/operations.js | 38 +++++++++++ src/editor/range.js | 4 ++ 5 files changed, 55 insertions(+), 57 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 39d24b1bab..38cfff9b65 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -22,9 +22,11 @@ import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; import {setCaretPosition} from '../../../editor/caret'; import { + replaceRangeAndExpandSelection, + formatRangeAsQuote, formatInline, } from '../../../editor/operations'; -import {getCaretOffsetAndText, getRangeForSelection, getSelectionOffsetAndText} from '../../../editor/dom'; +import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; import Autocomplete from '../rooms/Autocomplete'; import {autoCompleteCreator} from '../../../editor/parts'; import {renderModel} from '../../../editor/render'; @@ -427,37 +429,6 @@ export default class BasicMessageEditor extends React.Component { return caretPosition; } - _replaceSelection(callback) { - const selection = document.getSelection(); - if (selection.isCollapsed) { - return; - } - const focusOffset = getSelectionOffsetAndText( - this._editorRef, - selection.focusNode, - selection.focusOffset, - ).offset; - const anchorOffset = getSelectionOffsetAndText( - this._editorRef, - selection.anchorNode, - selection.anchorOffset, - ).offset; - const {model} = this.props; - const focusPosition = focusOffset.asPosition(model); - const anchorPosition = anchorOffset.asPosition(model); - const range = model.startRange(focusPosition, anchorPosition); - const firstPosition = focusPosition.compare(anchorPosition) < 0 ? focusPosition : anchorPosition; - - model.transform(() => { - const oldLen = range.length; - const newParts = callback(range); - const addedLen = range.replace(newParts); - const lastOffset = firstPosition.asOffset(model); - lastOffset.offset += oldLen + addedLen; - return lastOffset.asPosition(model); - }); - } - _wrapSelectionAsInline(prefix, suffix = prefix) { const range = getRangeForSelection( this._editorRef, @@ -479,30 +450,11 @@ export default class BasicMessageEditor extends React.Component { } _formatQuote = () => { - const {model} = this.props; - const {partCreator} = this.props.model; - this._replaceSelection(range => { - const parts = range.parts; - parts.splice(0, 0, partCreator.plain("> ")); - const startsWithPartial = range.start.offset !== 0; - const isFirstPart = range.start.index === 0; - const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline"; - // prepend a newline if there is more text before the range on this line - if (startsWithPartial || (!isFirstPart && !previousIsNewline)) { - parts.splice(0, 0, partCreator.newline()); - } - // start at position 1 to make sure we skip the potentially inserted newline above, - // as we already inserted a quote sign for it above - for (let i = 1; i < parts.length; ++i) { - const part = parts[i]; - if (part.type === "newline") { - parts.splice(i + 1, 0, partCreator.plain("> ")); - } - } - parts.push(partCreator.newline()); - parts.push(partCreator.newline()); - return parts; - }); + const range = getRangeForSelection( + this._editorRef, + this.props.model, + document.getSelection()); + formatRangeAsQuote(range); } _formatCodeBlock = () => { diff --git a/src/editor/dom.js b/src/editor/dom.js index 03ee7f2cfc..e2a65be6ff 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -45,7 +45,7 @@ export function getCaretOffsetAndText(editor, sel) { return {caret: offset, text}; } -export function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { +function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { // sometimes selectionNode is an element, and then selectionOffset means // the index of a child element ... - 1 🤷 if (selectionNode.nodeType === Node.ELEMENT_NODE && selectionOffset !== 0) { diff --git a/src/editor/offset.js b/src/editor/offset.js index c638640f6f..785f16bc6d 100644 --- a/src/editor/offset.js +++ b/src/editor/offset.js @@ -23,4 +23,8 @@ export default class DocumentOffset { asPosition(model) { return model.positionForOffset(this.offset, this.atNodeEnd); } + + add(delta, atNodeEnd = false) { + return new DocumentOffset(this.offset + delta, atNodeEnd); + } } diff --git a/src/editor/operations.js b/src/editor/operations.js index 4f0757948a..be979c275a 100644 --- a/src/editor/operations.js +++ b/src/editor/operations.js @@ -29,6 +29,44 @@ export function replaceRangeAndExpandSelection(model, range, newParts) { }); } +export function rangeStartsAtBeginningOfLine(range) { + const {model} = range; + const startsWithPartial = range.start.offset !== 0; + const isFirstPart = range.start.index === 0; + const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline"; + return !startsWithPartial && (isFirstPart || previousIsNewline); +} + +export function rangeEndsAtEndOfLine(range) { + const {model} = range; + const lastPart = model.parts[range.end.index]; + const endsWithPartial = range.end.offset !== lastPart.length; + const isLastPart = range.end.index === model.parts.length - 1; + const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === "newline"; + return !endsWithPartial && (isLastPart || nextIsNewline); +} + +export function formatRangeAsQuote(range) { + const {model, parts} = range; + const {partCreator} = model; + for (let i = 0; i < parts.length; ++i) { + const part = parts[i]; + if (part.type === "newline") { + parts.splice(i + 1, 0, partCreator.plain("> ")); + } + } + parts.unshift(partCreator.plain("> ")); + if (!rangeStartsAtBeginningOfLine(range)) { + parts.unshift(partCreator.newline()); + } + if (rangeEndsAtEndOfLine(range)) { + parts.push(partCreator.newline()); + } + + parts.push(partCreator.newline()); + replaceRangeAndExpandSelection(model, range, parts); +} + export function formatInline(range, prefix, suffix = prefix) { const {model, parts} = range; const {partCreator} = model; diff --git a/src/editor/range.js b/src/editor/range.js index 2163076515..0739cd7842 100644 --- a/src/editor/range.js +++ b/src/editor/range.js @@ -33,6 +33,10 @@ export default class Range { this._start = this._start.backwardsWhile(this._model, predicate); } + get model() { + return this._model; + } + get text() { let text = ""; this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { From 6e694c113ad65d5b458f39231b39daea12f4032b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 12:40:21 +0200 Subject: [PATCH 324/413] add support for inline/block code formatting --- .../views/rooms/BasicMessageComposer.js | 9 ++++++-- src/editor/operations.js | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 38cfff9b65..cd8589ee29 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -24,6 +24,7 @@ import {setCaretPosition} from '../../../editor/caret'; import { replaceRangeAndExpandSelection, formatRangeAsQuote, + formatRangeAsCode, formatInline, } from '../../../editor/operations'; import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; @@ -457,8 +458,12 @@ export default class BasicMessageEditor extends React.Component { formatRangeAsQuote(range); } - _formatCodeBlock = () => { - + _formatCode = () => { + const range = getRangeForSelection( + this._editorRef, + this.props.model, + document.getSelection()); + formatRangeAsCode(range); } render() { diff --git a/src/editor/operations.js b/src/editor/operations.js index be979c275a..df3047618b 100644 --- a/src/editor/operations.js +++ b/src/editor/operations.js @@ -67,6 +67,29 @@ export function formatRangeAsQuote(range) { replaceRangeAndExpandSelection(model, range, parts); } +export function formatRangeAsCode(range) { + const {model, parts} = range; + const {partCreator} = model; + const needsBlock = parts.some(p => p.type === "newline"); + if (needsBlock) { + parts.unshift(partCreator.plain("```"), partCreator.newline()); + if (!rangeStartsAtBeginningOfLine(range)) { + parts.unshift(partCreator.newline()); + } + parts.push( + partCreator.newline(), + partCreator.plain("```")); + if (rangeEndsAtEndOfLine(range)) { + parts.push(partCreator.newline()); + } + replaceRangeAndExpandSelection(model, range, parts); + } else { + parts.unshift(partCreator.plain("`")); + parts.push(partCreator.plain("`")); + replaceRangeAndExpandSelection(model, range, parts); + } +} + export function formatInline(range, prefix, suffix = prefix) { const {model, parts} = range; const {partCreator} = model; From 4c04bc19c9bf1716bcd490de2a4d1f4f71521ccb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 12:40:56 +0200 Subject: [PATCH 325/413] fixup: remove now unused import --- src/components/views/rooms/BasicMessageComposer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index cd8589ee29..b1eb4f0746 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -22,7 +22,6 @@ import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; import {setCaretPosition} from '../../../editor/caret'; import { - replaceRangeAndExpandSelection, formatRangeAsQuote, formatRangeAsCode, formatInline, From 7a01d1407f7042a8be65967fe7f62a41f4f06516 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 15:57:29 +0200 Subject: [PATCH 326/413] make _replaceRange internal only --- src/editor/model.js | 4 ++-- src/editor/range.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index 34a796f733..a121c67b48 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -401,8 +401,8 @@ export default class EditorModel { return new Range(this, positionA, positionB); } - //mostly internal, called from Range.replace - replaceRange(startPosition, endPosition, parts) { + // called from Range.replace + _replaceRange(startPosition, endPosition, parts) { // convert end position to offset, so it is independent of how the document is split into parts // which we'll change when splitting up at the start position const endOffset = endPosition.asOffset(this); diff --git a/src/editor/range.js b/src/editor/range.js index 0739cd7842..822c3b13a7 100644 --- a/src/editor/range.js +++ b/src/editor/range.js @@ -58,7 +58,7 @@ export default class Range { this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { oldLength += endIdx - startIdx; }); - this._model.replaceRange(this._start, this._end, parts); + this._model._replaceRange(this._start, this._end, parts); return newLength - oldLength; } From 4691108a1668de608385f14db30ae3a4a02bf155 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 15:58:13 +0200 Subject: [PATCH 327/413] only increase offset if caret hasn't been found yet also rename caret away as this isn't used for the caret solely anymore --- src/editor/dom.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/editor/dom.js b/src/editor/dom.js index e2a65be6ff..3096710166 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -80,13 +80,13 @@ function getCaret(node, offsetToNode, offsetWithinNode) { // all ZWS from caret nodes are filtered out function getTextAndOffsetToNode(editor, selectionNode) { let offsetToNode = 0; - let foundCaret = false; + let foundNode = false; let text = ""; function enterNodeCallback(node) { - if (!foundCaret) { + if (!foundNode) { if (node === selectionNode) { - foundCaret = true; + foundNode = true; } } // usually newlines are entered as new DIV elements, @@ -94,12 +94,14 @@ function getTextAndOffsetToNode(editor, selectionNode) { // converted to BRs, so also take these into account when they // are not the last element in the DIV. if (node.tagName === "BR" && node.nextSibling) { + if (!foundNode) { + offsetToNode += 1; + } text += "\n"; - offsetToNode += 1; } const nodeText = node.nodeType === Node.TEXT_NODE && getTextNodeValue(node); if (nodeText) { - if (!foundCaret) { + if (!foundNode) { offsetToNode += nodeText.length; } text += nodeText; @@ -114,7 +116,7 @@ function getTextAndOffsetToNode(editor, selectionNode) { // whereas you just want it to be appended to the current line if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") { text += "\n"; - if (!foundCaret) { + if (!foundNode) { offsetToNode += 1; } } From e0668e85175b39a1be168adcef0de259b3ad5fa8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 15:58:59 +0200 Subject: [PATCH 328/413] improve algorithm to reduce selection to text node + charactar offset this follows the documentation of {focus|anchor}{Offset|Node} better and was causing problems for creating ranges from the document selection when doing ctrl+A in firefox as the anchorNode/Offset was expressed as childNodes from the editor root. --- src/editor/dom.js | 48 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/editor/dom.js b/src/editor/dom.js index 3096710166..9073eb37a3 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -45,15 +45,47 @@ export function getCaretOffsetAndText(editor, sel) { return {caret: offset, text}; } -function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { - // sometimes selectionNode is an element, and then selectionOffset means - // the index of a child element ... - 1 🤷 - if (selectionNode.nodeType === Node.ELEMENT_NODE && selectionOffset !== 0) { - selectionNode = selectionNode.childNodes[selectionOffset - 1]; - selectionOffset = selectionNode.textContent.length; +function tryReduceSelectionToTextNode(selectionNode, selectionOffset) { + // if selectionNode is an element, the selected location comes after the selectionOffset-th child node, + // which can point past any childNode, in which case, the end of selectionNode is selected. + // we try to simplify this to point at a text node with the offset being + // a character offset within the text node + // Also see https://developer.mozilla.org/en-US/docs/Web/API/Selection + while (selectionNode && selectionNode.nodeType === Node.ELEMENT_NODE) { + const childNodeCount = selectionNode.childNodes.length; + if (childNodeCount) { + if (selectionOffset >= childNodeCount) { + selectionNode = selectionNode.lastChild; + if (selectionNode.nodeType === Node.TEXT_NODE) { + selectionOffset = selectionNode.textContent.length; + } else { + // this will select the last child node in the next iteration + selectionOffset = Number.MAX_SAFE_INTEGER; + } + } else { + selectionNode = selectionNode.childNodes[selectionOffset]; + // this will select the first child node in the next iteration + selectionOffset = 0; + } + } else { + // here node won't be a text node, + // but characterOffset should be 0, + // this happens under some circumstances + // when the editor is empty. + // In this case characterOffset=0 is the right thing to do + break; + } } - const {text, offsetToNode} = getTextAndOffsetToNode(editor, selectionNode); - const offset = getCaret(selectionNode, offsetToNode, selectionOffset); + return { + node: selectionNode, + characterOffset: selectionOffset, + }; +} + +function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { + const {node, characterOffset} = tryReduceSelectionToTextNode(selectionNode, selectionOffset); + const {text, offsetToNode} = getTextAndOffsetToNode(editor, node); + const offset = getCaret(node, offsetToNode, characterOffset); return {offset, text}; } From 42c37d829310b7b5ffea96a484879dc87b68b685 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 16:01:28 +0200 Subject: [PATCH 329/413] fixup: improve quote and code block newline handling --- src/editor/operations.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/editor/operations.js b/src/editor/operations.js index df3047618b..a8c9ac6d25 100644 --- a/src/editor/operations.js +++ b/src/editor/operations.js @@ -59,7 +59,7 @@ export function formatRangeAsQuote(range) { if (!rangeStartsAtBeginningOfLine(range)) { parts.unshift(partCreator.newline()); } - if (rangeEndsAtEndOfLine(range)) { + if (!rangeEndsAtEndOfLine(range)) { parts.push(partCreator.newline()); } @@ -79,15 +79,14 @@ export function formatRangeAsCode(range) { parts.push( partCreator.newline(), partCreator.plain("```")); - if (rangeEndsAtEndOfLine(range)) { + if (!rangeEndsAtEndOfLine(range)) { parts.push(partCreator.newline()); } - replaceRangeAndExpandSelection(model, range, parts); } else { parts.unshift(partCreator.plain("`")); parts.push(partCreator.plain("`")); - replaceRangeAndExpandSelection(model, range, parts); } + replaceRangeAndExpandSelection(model, range, parts); } export function formatInline(range, prefix, suffix = prefix) { From 037ac29c578257c7a86b0c766bda9f8c3cef7f4b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 16:02:07 +0200 Subject: [PATCH 330/413] be more forgiving with offset that don't have atNodeEnd=true if index is not found, it means the last position should be returned if there is any. We still return -1 for empty documents, as index should always point to a valid part if positive. --- src/editor/model.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/editor/model.js b/src/editor/model.js index a121c67b48..3b4f1ce460 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -388,7 +388,11 @@ export default class EditorModel { currentOffset += partLen; return false; }); - return new DocumentPosition(index, totalOffset - currentOffset); + if (index === -1) { + return this.getPositionAtEnd(); + } else { + return new DocumentPosition(index, totalOffset - currentOffset); + } } /** From 2ea556e0b40897fb92785a956f1b97baa204d41e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 16:04:06 +0200 Subject: [PATCH 331/413] support update callback setting selection instead of caret --- .../views/rooms/BasicMessageComposer.js | 10 +++--- src/editor/caret.js | 33 ++++++++++++++++--- src/editor/model.js | 7 +++- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index b1eb4f0746..3f07cf567e 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -20,7 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; -import {setCaretPosition} from '../../../editor/caret'; +import {setSelection} from '../../../editor/caret'; import { formatRangeAsQuote, formatRangeAsCode, @@ -115,11 +115,11 @@ export default class BasicMessageEditor extends React.Component { } } - _updateEditorState = (caret, inputType, diff) => { + _updateEditorState = (selection, inputType, diff) => { renderModel(this._editorRef, this.props.model); - if (caret) { + if (selection) { // set the caret/selection try { - setCaretPosition(this._editorRef, this.props.model, caret); + setSelection(this._editorRef, this.props.model, selection); } catch (err) { console.error(err); } @@ -133,7 +133,7 @@ export default class BasicMessageEditor extends React.Component { } } this.setState({autoComplete: this.props.model.autoComplete}); - this.historyManager.tryPush(this.props.model, caret, inputType, diff); + this.historyManager.tryPush(this.props.model, selection, inputType, diff); TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty); if (this.props.onChange) { diff --git a/src/editor/caret.js b/src/editor/caret.js index 9b0fa14cfc..ed4f1b2a2e 100644 --- a/src/editor/caret.js +++ b/src/editor/caret.js @@ -16,12 +16,39 @@ limitations under the License. */ import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render"; +import Range from "./range"; + +export function setSelection(editor, model, selection) { + if (selection instanceof Range) { + setDocumentRangeSelection(editor, model, selection); + } else { + setCaretPosition(editor, model, selection); + } +} + +function setDocumentRangeSelection(editor, model, range) { + const sel = document.getSelection(); + sel.removeAllRanges(); + const selectionRange = document.createRange(); + const start = getNodeAndOffsetForPosition(editor, model, range.start); + selectionRange.setStart(start.node, start.offset); + const end = getNodeAndOffsetForPosition(editor, model, range.end); + selectionRange.setEnd(end.node, end.offset); + sel.addRange(selectionRange); +} export function setCaretPosition(editor, model, caretPosition) { const sel = document.getSelection(); sel.removeAllRanges(); const range = document.createRange(); - const {offset, lineIndex, nodeIndex} = getLineAndNodePosition(model, caretPosition); + const {node, offset} = getNodeAndOffsetForPosition(editor, model, caretPosition); + range.setStart(node, offset); + range.collapse(true); + sel.addRange(range); +} + +function getNodeAndOffsetForPosition(editor, model, position) { + const {offset, lineIndex, nodeIndex} = getLineAndNodePosition(model, position); const lineNode = editor.childNodes[lineIndex]; let focusNode; @@ -35,9 +62,7 @@ export function setCaretPosition(editor, model, caretPosition) { focusNode = focusNode.firstChild; } } - range.setStart(focusNode, offset); - range.collapse(true); - sel.addRange(range); + return {node: focusNode, offset}; } export function getLineAndNodePosition(model, caretPosition) { diff --git a/src/editor/model.js b/src/editor/model.js index 3b4f1ce460..ea6b05570c 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -433,7 +433,12 @@ export default class EditorModel { */ transform(callback) { const pos = callback(); - const acPromise = this._setActivePart(pos, true); + let acPromise = null; + if (!(pos instanceof Range)) { + acPromise = this._setActivePart(pos, true); + } else { + acPromise = Promise.resolve(); + } this._updateCallback(pos); return acPromise; } From af535986d2f345e8bab914ad83fe6e8d620e4eac Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 4 Sep 2019 16:21:53 +0200 Subject: [PATCH 332/413] fix css lint --- res/css/views/rooms/_BasicMessageComposer.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index 72a10bf074..a90097846c 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -76,13 +76,12 @@ limitations under the License. .mx_BasicMessageComposer_formatBar { display: none; - background-color: red; width: calc(26px * 5); height: 24px; position: absolute; cursor: pointer; border-radius: 4px; - background: $message-action-bar-bg-color; + background-color: $message-action-bar-bg-color; &.mx_BasicMessageComposer_formatBar_shown { display: block; @@ -137,6 +136,5 @@ limitations under the License. .mx_BasicMessageComposer_formatCode::after { mask-image: url('$(res)/img/format/code.svg'); } - } } From d6adf0fd6db41845ee548b137ce4fde3a0b1cdd3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 4 Sep 2019 11:24:31 -0600 Subject: [PATCH 333/413] Add responsible error handling because we're responsible people --- .../settings/tabs/room/RolesRoomSettingsTab.js | 13 ++++++++++++- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js index f76bd6efa2..002748694c 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js @@ -164,7 +164,18 @@ export default class RolesRoomSettingsTab extends React.Component { if (!plContent['users']) plContent['users'] = {}; plContent['users'][powerLevelKey] = value; - client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent); + client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Power level change failed', '', ErrorDialog, { + title: _t('Error changing power level'), + description: _t( + "An error occurred changing the user's power level. Ensure you have sufficient " + + "permissions and try again.", + ), + }); + }); }; render() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2498b018e3..03e586c4e4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -689,6 +689,8 @@ "Failed to unban": "Failed to unban", "Unban": "Unban", "Banned by %(displayName)s": "Banned by %(displayName)s", + "Error changing power level": "Error changing power level", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.", "Default role": "Default role", "Send messages": "Send messages", "Invite users": "Invite users", From e3d70f39997fff227bafa0bee08c194060e084c0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 11:01:44 +0200 Subject: [PATCH 334/413] ensure selection is not lost upon clicking format bar in chrome --- res/css/views/rooms/_BasicMessageComposer.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index a90097846c..e897352d7e 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -82,6 +82,7 @@ limitations under the License. cursor: pointer; border-radius: 4px; background-color: $message-action-bar-bg-color; + user-select: none; &.mx_BasicMessageComposer_formatBar_shown { display: block; From 4ef9fa53ac6f8cf395c432fd20d2102fd821643b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 11:10:13 +0200 Subject: [PATCH 335/413] better i18n --- src/components/views/rooms/BasicMessageComposer.js | 11 ++++++----- src/i18n/strings/en_EN.json | 6 +++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 3f07cf567e..06d8013550 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -35,6 +35,7 @@ import TypingStore from "../../../stores/TypingStore"; import EMOJIBASE from 'emojibase-data/en/compact.json'; import SettingsStore from "../../../settings/SettingsStore"; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; +import { _t } from '../../../languageHandler'; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -487,11 +488,11 @@ export default class BasicMessageEditor extends React.Component { return (
{ autoComplete }
this._formatBarRef = ref}> - - - - - + + + + +
voice or video.": "Join as voice or video.", "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.", + "Bold": "Bold", + "Italics": "Italics", + "Strikethrough": "Strikethrough", + "Code block": "Code block", + "Quote": "Quote", "Some devices for this user are not trusted": "Some devices for this user are not trusted", "Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted", "All devices for this user are trusted": "All devices for this user are trusted", @@ -1397,7 +1402,6 @@ "Unhide Preview": "Unhide Preview", "Share Permalink": "Share Permalink", "Share Message": "Share Message", - "Quote": "Quote", "Source URL": "Source URL", "Collapse Reply Thread": "Collapse Reply Thread", "End-to-end encryption information": "End-to-end encryption information", From 0929a9cc7287c800d3ea88b22a1d6bce13487756 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 5 Sep 2019 13:38:32 +0100 Subject: [PATCH 336/413] Add new agreed URLs to account data instead of overwriting This changes terms account data storage to always add, rather than setting only the current set of displayed URLs. Fixes https://github.com/vector-im/riot-web/issues/10755 --- src/Terms.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Terms.js b/src/Terms.js index 02e34cbb3f..594f15b522 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -119,7 +119,8 @@ export async function startTermsFlow( if (unagreedPoliciesAndServicePairs.length > 0) { const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]); console.log("User has agreed to URLs", newlyAgreedUrls); - agreedUrlSet = new Set(newlyAgreedUrls); + // Merge with previously agreed URLs + newlyAgreedUrls.forEach(url => agreedUrlSet.add(url)); } else { console.log("User has already agreed to all required policies"); } From 39bbf9af24e9ec83102152b910b887b8cbdfbe10 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 15:19:28 +0200 Subject: [PATCH 337/413] remove accent color as selection color --- res/css/_common.scss | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 859c0006a1..adf4c93290 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -456,16 +456,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { background-color: $primary-bg-color; } -::-moz-selection { - background-color: $accent-color; - color: $selection-fg-color; -} - -::selection { - background-color: $accent-color; - color: $selection-fg-color; -} - .mx_textButton { @mixin mx_DialogButton_small; } From 124b7135cd794d0adb6be7134be922cdd93498b3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 17:54:08 +0200 Subject: [PATCH 338/413] make sure the update callback gets a caret when calling reset --- src/editor/model.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/editor/model.js b/src/editor/model.js index 613be5b4bd..5da3477443 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -149,6 +149,9 @@ export default class EditorModel { reset(serializedParts, caret, inputType) { this._parts = serializedParts.map(p => this._partCreator.deserializePart(p)); + if (!caret) { + caret = this.getPositionAtEnd(); + } // close auto complete if open // this would happen when clearing the composer after sending // a message with the autocomplete still open From d5552e4a179ccd975367d412168430518779d797 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 5 Sep 2019 17:51:27 +0100 Subject: [PATCH 339/413] Add bound 3PID warning when changing IS as well This extends the bound 3PID warning from the disconnect button to also appear when changing the IS as well. At the moment, the text is a bit terse, but will be improved separately. Fixes https://github.com/vector-im/riot-web/issues/10749 --- src/components/views/settings/SetIdServer.js | 134 ++++++++++++------- src/i18n/strings/en_EN.json | 6 +- 2 files changed, 87 insertions(+), 53 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index a718d87fa6..e3d6d18c5b 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -137,7 +137,12 @@ export default class SetIdServer extends React.Component { MatrixClientPeg.get().setAccountData("m.identity_server", { base_url: fullUrl, }); - this.setState({idServer: '', busy: false, error: null}); + this.setState({ + busy: false, + error: null, + currentClientIdServer: fullUrl, + idServer: '', + }); }; _checkIdServer = async (e) => { @@ -157,14 +162,34 @@ export default class SetIdServer extends React.Component { const authClient = new IdentityAuthClient(fullUrl); await authClient.getAccessToken(); + let save = true; + // Double check that the identity server even has terms of service. const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { - this._showNoTermsWarning(fullUrl); - return; + save &= await this._showNoTermsWarning(fullUrl); } - this._saveIdServer(fullUrl); + // Show a general warning, possibly with details about any bound + // 3PIDs that would be left behind. + if (this.state.currentClientIdServer) { + save &= await this._showServerChangeWarning({ + title: _t("Change identity server"), + unboundMessage: _t( + "Disconnect from the identity server and " + + "connect to instead?", {}, + { + current: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + new: sub => {abbreviateUrl(this.state.idServer)}, + }, + ), + button: _t("Continue"), + }); + } + + if (save) { + this._saveIdServer(fullUrl); + } } catch (e) { console.error(e); if (e.cors === "rejected" || e.httpStatus === 404) { @@ -179,73 +204,80 @@ export default class SetIdServer extends React.Component { checking: false, error: errStr, currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), - idServer: this.state.idServer, }); }; _showNoTermsWarning(fullUrl) { const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); - Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { - title: _t("Identity server has no terms of service"), - description: ( -
- - {_t("The identity server you have chosen does not have any terms of service.")} - - -  {_t("Only continue if you trust the owner of the server.")} - -
- ), - button: _t("Continue"), - onFinished: async (confirmed) => { - if (!confirmed) return; - this._saveIdServer(fullUrl); - }, + return new Promise(resolve => { + Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { + title: _t("Identity server has no terms of service"), + description: ( +
+ + {_t("The identity server you have chosen does not have any terms of service.")} + + +  {_t("Only continue if you trust the owner of the server.")} + +
+ ), + button: _t("Continue"), + onFinished: resolve, + }); }); } _onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); try { - const threepids = await getThreepidBindStatus(MatrixClientPeg.get()); - - const boundThreepids = threepids.filter(tp => tp.bound); - let message; - if (boundThreepids.length) { - message = _t( - "You are currently sharing email addresses or phone numbers on the identity " + - "server . You will need to reconnect to to stop " + - "sharing them.", {}, - { - idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - // XXX: https://github.com/vector-im/riot-web/issues/9086 - idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - }, - ); - } else { - message = _t( + const confirmed = await this._showServerChangeWarning({ + title: _t("Disconnect identity server"), + unboundMessage: _t( "Disconnect from the identity server ?", {}, {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, - ); - } - - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Identity Server Disconnect Warning', '', QuestionDialog, { - title: _t("Disconnect Identity Server"), - description: message, + ), button: _t("Disconnect"), - onFinished: (confirmed) => { - if (confirmed) { - this._disconnectIdServer(); - } - }, }); + if (confirmed) { + this._disconnectIdServer(); + } } finally { this.setState({disconnectBusy: false}); } }; + async _showServerChangeWarning({ title, unboundMessage, button }) { + const threepids = await getThreepidBindStatus(MatrixClientPeg.get()); + + const boundThreepids = threepids.filter(tp => tp.bound); + let message; + if (boundThreepids.length) { + message = _t( + "You are currently sharing email addresses or phone numbers on the identity " + + "server . You will need to reconnect to to stop " + + "sharing them.", {}, + { + idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + // XXX: https://github.com/vector-im/riot-web/issues/9086 + idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + }, + ); + } else { + message = unboundMessage; + } + + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + return new Promise(resolve => { + Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, { + title, + description: message, + button, + onFinished: resolve, + }); + }); + } + _disconnectIdServer = () => { // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dd0894b813..f31fcc7157 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -554,14 +554,16 @@ "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", "Could not connect to Identity Server": "Could not connect to Identity Server", "Checking server": "Checking server", + "Change identity server": "Change identity server", + "Disconnect from the identity server and connect to instead?": "Disconnect from the identity server and connect to instead?", "Terms of service not accepted or the identity server is invalid.": "Terms of service not accepted or the identity server is invalid.", "Identity server has no terms of service": "Identity server has no terms of service", "The identity server you have chosen does not have any terms of service.": "The identity server you have chosen does not have any terms of service.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", - "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", + "Disconnect identity server": "Disconnect identity server", "Disconnect from the identity server ?": "Disconnect from the identity server ?", - "Disconnect Identity Server": "Disconnect Identity Server", "Disconnect": "Disconnect", + "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.", From af35cdc2ea87fe38b8fd70996cf3e5279918feea Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 5 Sep 2019 20:30:19 -0600 Subject: [PATCH 340/413] Support sending hidden read receipts Fixes https://github.com/vector-im/riot-web/issues/2527 --- src/components/structures/TimelinePanel.js | 5 +++++ .../views/settings/tabs/user/LabsUserSettingsTab.js | 1 + src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 7 +++++++ 4 files changed, 14 insertions(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index cdeea78204..44569569b6 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -685,20 +685,25 @@ const TimelinePanel = createReactClass({ } this.lastRMSentEventId = this.state.readMarkerEventId; + const hiddenRR = !SettingsStore.getValue("sendReadReceipts"); + debuglog('TimelinePanel: Sending Read Markers for ', this.props.timelineSet.room.roomId, 'rm', this.state.readMarkerEventId, lastReadEvent ? 'rr ' + lastReadEvent.getId() : '', + ' hidden:' + hiddenRR, ); MatrixClientPeg.get().setRoomReadMarkers( this.props.timelineSet.room.roomId, this.state.readMarkerEventId, lastReadEvent, // Could be null, in which case no RR is sent + {hidden: hiddenRR}, ).catch((e) => { // /read_markers API is not implemented on this HS, fallback to just RR if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) { return MatrixClientPeg.get().sendReadReceipt( lastReadEvent, + {hidden: hiddenRR}, ).catch((e) => { console.error(e); this.lastRRSentEventId = undefined; diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 9c2d49a8bf..07a2bf722a 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -54,6 +54,7 @@ export default class LabsUserSettingsTab extends React.Component { +
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dd0894b813..acccade834 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -367,6 +367,7 @@ "Show hidden events in timeline": "Show hidden events in timeline", "Low bandwidth mode": "Low bandwidth mode", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)", + "Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 70abf406b8..f86a8566c6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -394,4 +394,11 @@ export const SETTINGS = { // This is a tri-state value, where `null` means "prompt the user". default: null, }, + "sendReadReceipts": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td( + "Send read receipts for messages (requires compatible homeserver to disable)", + ), + default: true, + }, }; From d2949babcd9a60328ce595f6baf95ea84ec0a2a2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 6 Sep 2019 10:28:20 +0200 Subject: [PATCH 341/413] copyright is solely assigned to matrix foundation now, copy paste error --- src/editor/operations.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/editor/operations.js b/src/editor/operations.js index a8c9ac6d25..e6ed2ba0e5 100644 --- a/src/editor/operations.js +++ b/src/editor/operations.js @@ -1,5 +1,4 @@ /* -Copyright 2019 New Vector Ltd Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); From 92c0c1a4e2c1a38bba1a92fe0d0ca1d85d76db36 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 6 Sep 2019 10:28:53 +0200 Subject: [PATCH 342/413] add comment about positioning the format bar --- src/components/views/rooms/BasicMessageComposer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 06d8013550..e0468e9969 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -274,6 +274,7 @@ export default class BasicMessageEditor extends React.Component { } this._formatBarRef.style.left = `${selectionRect.left - leftOffset}px`; + // 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok. this._formatBarRef.style.top = `${selectionRect.top - topOffset - 16 - 12}px`; } } From da29057fd8a98e8c922d1972f8f565e0a7df4b27 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 12:10:45 +0200 Subject: [PATCH 343/413] move format bar to own component --- res/css/_components.scss | 1 + .../views/rooms/_BasicMessageComposer.scss | 65 --------------- .../rooms/_MessageComposerFormatBar.scss | 81 ++++++++++++++++++ .../views/rooms/BasicMessageComposer.js | 83 ++++++------------- .../views/rooms/MessageComposerFormatBar.js | 79 ++++++++++++++++++ 5 files changed, 186 insertions(+), 123 deletions(-) create mode 100644 res/css/views/rooms/_MessageComposerFormatBar.scss create mode 100644 src/components/views/rooms/MessageComposerFormatBar.js diff --git a/res/css/_components.scss b/res/css/_components.scss index fb6058df00..213d0d714c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -146,6 +146,7 @@ @import "./views/rooms/_MemberInfo.scss"; @import "./views/rooms/_MemberList.scss"; @import "./views/rooms/_MessageComposer.scss"; +@import "./views/rooms/_MessageComposerFormatBar.scss"; @import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PresenceLabel.scss"; diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e897352d7e..b32a44219a 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -73,69 +73,4 @@ limitations under the License. position: relative; height: 0; } - - .mx_BasicMessageComposer_formatBar { - display: none; - width: calc(26px * 5); - height: 24px; - position: absolute; - cursor: pointer; - border-radius: 4px; - background-color: $message-action-bar-bg-color; - user-select: none; - - &.mx_BasicMessageComposer_formatBar_shown { - display: block; - } - - > * { - white-space: nowrap; - display: inline-block; - position: relative; - border: 1px solid $message-action-bar-border-color; - margin-left: -1px; - - &:hover { - border-color: $message-action-bar-hover-border-color; - } - } - - .mx_BasicMessageComposer_formatButton { - width: 27px; - height: 24px; - box-sizing: border-box; - } - - .mx_BasicMessageComposer_formatButton::after { - content: ''; - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - mask-repeat: no-repeat; - mask-position: center; - background-color: $message-action-bar-fg-color; - } - - .mx_BasicMessageComposer_formatBold::after { - mask-image: url('$(res)/img/format/bold.svg'); - } - - .mx_BasicMessageComposer_formatItalic::after { - mask-image: url('$(res)/img/format/italics.svg'); - } - - .mx_BasicMessageComposer_formatStrikethrough::after { - mask-image: url('$(res)/img/format/strikethrough.svg'); - } - - .mx_BasicMessageComposer_formatQuote::after { - mask-image: url('$(res)/img/format/quote.svg'); - } - - .mx_BasicMessageComposer_formatCode::after { - mask-image: url('$(res)/img/format/code.svg'); - } - } } diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss new file mode 100644 index 0000000000..2e74076e2a --- /dev/null +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -0,0 +1,81 @@ +/* +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. +*/ + +.mx_MessageComposerFormatBar { + display: none; + width: calc(26px * 5); + height: 24px; + position: absolute; + cursor: pointer; + border-radius: 4px; + background-color: $message-action-bar-bg-color; + user-select: none; + + &.mx_MessageComposerFormatBar_shown { + display: block; + } + + > * { + white-space: nowrap; + display: inline-block; + position: relative; + border: 1px solid $message-action-bar-border-color; + margin-left: -1px; + + &:hover { + border-color: $message-action-bar-hover-border-color; + } + } + + .mx_MessageComposerFormatBar_button { + width: 27px; + height: 24px; + box-sizing: border-box; + } + + .mx_MessageComposerFormatBar_button::after { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + mask-repeat: no-repeat; + mask-position: center; + background-color: $message-action-bar-fg-color; + } + + .mx_MessageComposerFormatBar_buttonIconBold::after { + mask-image: url('$(res)/img/format/bold.svg'); + } + + .mx_MessageComposerFormatBar_buttonIconItalic::after { + mask-image: url('$(res)/img/format/italics.svg'); + } + + .mx_MessageComposerFormatBar_buttonIconStrikethrough::after { + mask-image: url('$(res)/img/format/strikethrough.svg'); + } + + .mx_MessageComposerFormatBar_buttonIconQuote::after { + mask-image: url('$(res)/img/format/quote.svg'); + } + + .mx_MessageComposerFormatBar_buttonIconCode::after { + mask-image: url('$(res)/img/format/code.svg'); + } +} diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index e0468e9969..b37552da2a 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -35,7 +35,7 @@ import TypingStore from "../../../stores/TypingStore"; import EMOJIBASE from 'emojibase-data/en/compact.json'; import SettingsStore from "../../../settings/SettingsStore"; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; -import { _t } from '../../../languageHandler'; +import sdk from '../../../index'; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -251,31 +251,13 @@ export default class BasicMessageEditor extends React.Component { if (this._hasTextSelected && selection.isCollapsed) { this._hasTextSelected = false; if (this._formatBarRef) { - this._formatBarRef.classList.remove("mx_BasicMessageComposer_formatBar_shown"); + this._formatBarRef.hide(); } } else if (!selection.isCollapsed) { this._hasTextSelected = true; if (this._formatBarRef) { - this._formatBarRef.classList.add("mx_BasicMessageComposer_formatBar_shown"); const selectionRect = selection.getRangeAt(0).getBoundingClientRect(); - - let leftOffset = 0; - let node = this._formatBarRef; - while (node.offsetParent) { - node = node.offsetParent; - leftOffset += node.offsetLeft; - } - - let topOffset = 0; - node = this._formatBarRef; - while (node.offsetParent) { - node = node.offsetParent; - topOffset += node.offsetTop; - } - - this._formatBarRef.style.left = `${selectionRect.left - leftOffset}px`; - // 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok. - this._formatBarRef.style.top = `${selectionRect.top - topOffset - 16 - 12}px`; + this._formatBarRef.showAt(selectionRect); } } } @@ -431,40 +413,28 @@ export default class BasicMessageEditor extends React.Component { return caretPosition; } - _wrapSelectionAsInline(prefix, suffix = prefix) { + _onFormatAction = (action) => { const range = getRangeForSelection( this._editorRef, this.props.model, document.getSelection()); - formatInline(range, prefix, suffix); - } - - _formatBold = () => { - this._wrapSelectionAsInline("**"); - } - - _formatItalic = () => { - this._wrapSelectionAsInline("*"); - } - - _formatStrikethrough = () => { - this._wrapSelectionAsInline("", ""); - } - - _formatQuote = () => { - const range = getRangeForSelection( - this._editorRef, - this.props.model, - document.getSelection()); - formatRangeAsQuote(range); - } - - _formatCode = () => { - const range = getRangeForSelection( - this._editorRef, - this.props.model, - document.getSelection()); - formatRangeAsCode(range); + switch (action) { + case "bold": + formatInline(range, "**"); + break; + case "italics": + formatInline(range, "*"); + break; + case "strikethrough": + formatInline(range, "", ""); + break; + case "code": + formatRangeAsCode(range); + break; + case "quote": + formatRangeAsQuote(range); + break; + } } render() { @@ -486,15 +456,12 @@ export default class BasicMessageEditor extends React.Component { const classes = classNames("mx_BasicMessageComposer", { "mx_BasicMessageComposer_input_error": this.state.showVisualBell, }); + + const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar'); + return (
{ autoComplete } -
this._formatBarRef = ref}> - - - - - -
+ this._formatBarRef = ref} onAction={this._onFormatAction} />
this._formatBarRef = ref}> + this.props.onAction("bold")} icon="Bold" /> + this.props.onAction("italics")} icon="Italic" /> + this.props.onAction("strikethrough")} icon="Strikethrough" /> + this.props.onAction("code")} icon="Code" /> + this.props.onAction("quote")} icon="Quote" /> +
); + } + + showAt(selectionRect) { + this._formatBarRef.classList.add("mx_MessageComposerFormatBar_shown"); + let leftOffset = 0; + let node = this._formatBarRef; + while (node.offsetParent) { + node = node.offsetParent; + leftOffset += node.offsetLeft; + } + + let topOffset = 0; + node = this._formatBarRef; + while (node.offsetParent) { + node = node.offsetParent; + topOffset += node.offsetTop; + } + + this._formatBarRef.style.left = `${selectionRect.left - leftOffset}px`; + // 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok. + this._formatBarRef.style.top = `${selectionRect.top - topOffset - 16 - 12}px`; + } + + hide() { + this._formatBarRef.classList.remove("mx_MessageComposerFormatBar_shown"); + } +} + +class FormatButton extends React.PureComponent { + static propTypes = { + label: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + icon: PropTypes.string.isRequired, + } + + render() { + const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`; + return ( + + + ); + } +} From b4b9c7d07244136e12b4e0792a0d602f2ee5e9a8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 13:22:42 +0200 Subject: [PATCH 344/413] Add tooltip for format buttons --- .../rooms/_MessageComposerFormatBar.scss | 6 ++++++ .../views/rooms/MessageComposerFormatBar.js | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss index 2e74076e2a..c8ca218b13 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -79,3 +79,9 @@ limitations under the License. mask-image: url('$(res)/img/format/code.svg'); } } + +.mx_MessageComposerFormatBar_buttonTooltip { + white-space: nowrap; + font-size: 12px; + font-weight: 600; +} diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.js index cc3f341653..154c8b0d31 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.js +++ b/src/components/views/rooms/MessageComposerFormatBar.js @@ -17,6 +17,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; +import sdk from '../../../index'; + export default class MessageComposerFormatBar extends React.PureComponent { static propTypes = { @@ -67,13 +69,20 @@ class FormatButton extends React.PureComponent { } render() { + const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip'); const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`; + const tooltipContent = ( +
{this.props.label}
+ ); + return ( - - + + + + ); } } From d6a493a2b11e3cdfb969eb990054b6ee6ce776fd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 13:27:33 +0200 Subject: [PATCH 345/413] fixup: language strings moved --- src/i18n/strings/en_EN.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d42734d5a4..6529e7322c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -760,11 +760,6 @@ " (unsupported)": " (unsupported)", "Join as voice or video.": "Join as voice or video.", "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.", - "Bold": "Bold", - "Italics": "Italics", - "Strikethrough": "Strikethrough", - "Code block": "Code block", - "Quote": "Quote", "Some devices for this user are not trusted": "Some devices for this user are not trusted", "Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted", "All devices for this user are trusted": "All devices for this user are trusted", @@ -836,6 +831,11 @@ "The conversation continues here.": "The conversation continues here.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "You do not have permission to post to this room": "You do not have permission to post to this room", + "Bold": "Bold", + "Italics": "Italics", + "Strikethrough": "Strikethrough", + "Code block": "Code block", + "Quote": "Quote", "Server error": "Server error", "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", "Command error": "Command error", From 042822c90a3ea71e3c67be9468f6673d1241242e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 6 Sep 2019 10:37:28 +0200 Subject: [PATCH 346/413] copyright is solely assigned to matrix foundation now, copy paste error --- res/css/views/rooms/_MessageComposerFormatBar.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss index c8ca218b13..f56214224d 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -1,5 +1,4 @@ /* -Copyright 2019 New Vector Ltd Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); From bdcea6f21eb59592e894402d519d72f44b745b2e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 15:07:12 +0200 Subject: [PATCH 347/413] add shortcuts for formatting --- src/components/views/rooms/BasicMessageComposer.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index b37552da2a..dcb54d6cc2 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -266,8 +266,20 @@ export default class BasicMessageEditor extends React.Component { const model = this.props.model; const modKey = IS_MAC ? event.metaKey : event.ctrlKey; let handled = false; + // format bold + if (modKey && event.key === "b") { + this._onFormatAction("bold"); + handled = true; + // format italics + } else if (modKey && event.key === "i") { + this._onFormatAction("italics"); + handled = true; + // format quote + } else if (modKey && event.key === ">") { + this._onFormatAction("quote"); + handled = true; // undo - if (modKey && event.key === "z") { + } else if (modKey && event.key === "z") { if (this.historyManager.canUndo()) { const {parts, caret} = this.historyManager.undo(this.props.model); // pass matching inputType so historyManager doesn't push echo From 06143ba7a1767015eba78449a23e11fba33b71db Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 15:09:37 +0200 Subject: [PATCH 348/413] show keyboard shortcuts in format button tooltip --- .../views/rooms/_MessageComposerFormatBar.scss | 9 ++++++++- .../views/rooms/BasicMessageComposer.js | 11 ++++++++++- .../views/rooms/MessageComposerFormatBar.js | 17 +++++++++++++---- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss index f56214224d..6e8fc58470 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -81,6 +81,13 @@ limitations under the License. .mx_MessageComposerFormatBar_buttonTooltip { white-space: nowrap; - font-size: 12px; + font-size: 13px; font-weight: 600; + min-width: 54px; + text-align: center; + + .mx_MessageComposerFormatBar_tooltipShortcut { + font-size: 9px; + opacity: 0.7; + } } diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index dcb54d6cc2..3d2b96f773 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -41,6 +41,10 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc const IS_MAC = navigator.platform.indexOf("Mac") !== -1; +function ctrlShortcutLabel(key) { + return (IS_MAC ? "⌘" : "Ctrl") + "+" + key; +} + function cloneSelection(selection) { return { anchorNode: selection.anchorNode, @@ -470,10 +474,15 @@ export default class BasicMessageEditor extends React.Component { }); const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar'); + const shortcuts = { + bold: ctrlShortcutLabel("B"), + italics: ctrlShortcutLabel("I"), + quote: ctrlShortcutLabel(">"), + }; return (
{ autoComplete } - this._formatBarRef = ref} onAction={this._onFormatAction} /> + this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} />
this._formatBarRef = ref}> - this.props.onAction("bold")} icon="Bold" /> - this.props.onAction("italics")} icon="Italic" /> + this.props.onAction("bold")} icon="Bold" /> + this.props.onAction("italics")} icon="Italic" /> this.props.onAction("strikethrough")} icon="Strikethrough" /> this.props.onAction("code")} icon="Code" /> - this.props.onAction("quote")} icon="Quote" /> + this.props.onAction("quote")} icon="Quote" />
); } @@ -66,13 +67,21 @@ class FormatButton extends React.PureComponent { label: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, icon: PropTypes.string.isRequired, + shortcut: PropTypes.string, } render() { const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip'); const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`; + let shortcut; + if (this.props.shortcut) { + shortcut =
{this.props.shortcut}
; + } const tooltipContent = ( -
{this.props.label}
+
+
{this.props.label}
+ {shortcut} +
); return ( From f4938f9d118d0a670e9ae6619f207787a97381bf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 15:17:33 +0200 Subject: [PATCH 349/413] dont format empty ranges --- src/components/views/rooms/BasicMessageComposer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 3d2b96f773..36e142c0ea 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -434,6 +434,9 @@ export default class BasicMessageEditor extends React.Component { this._editorRef, this.props.model, document.getSelection()); + if (range.length === 0) { + return; + } switch (action) { case "bold": formatInline(range, "**"); From 2596281a7c5aba1c6a1e13713b52fd96c78c7f5b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 6 Sep 2019 11:09:01 +0200 Subject: [PATCH 350/413] update last caret from update callback instead of input event many editor updates are not caused by an input event, so the last caret wasn't always up to date. Updating the caret from the update callback ensures that every time the editor contents is changed, the last caret is updated. --- .../views/rooms/BasicMessageComposer.js | 17 ++++++++++------- src/editor/position.js | 13 +++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index c5661e561c..2764a8da46 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -116,6 +116,9 @@ export default class BasicMessageEditor extends React.Component { } catch (err) { console.error(err); } + // if caret is a range, take the end position + const position = caret.end || caret; + this._setLastCaretFromPosition(position); } if (this.props.placeholder) { const {isEmpty} = this.props.model; @@ -165,7 +168,6 @@ export default class BasicMessageEditor extends React.Component { this._modifiedFlag = true; const sel = document.getSelection(); const {caret, text} = getCaretOffsetAndText(this._editorRef, sel); - this._setLastCaret(caret, text, sel); this.props.model.update(text, event.inputType, caret); } @@ -183,10 +185,11 @@ export default class BasicMessageEditor extends React.Component { // we don't need to. But if the user is navigating the caret without input // we need to recalculate it, to be able to know where to insert content after // losing focus - _setLastCaret(caret, text, selection) { - this._lastSelection = cloneSelection(selection); - this._lastCaret = caret; - this._lastTextLength = text.length; + _setLastCaretFromPosition(position) { + const {model} = this.props; + this._isCaretAtEnd = position.isAtEnd(model); + this._lastCaret = position.asOffset(model); + this._lastSelection = cloneSelection(document.getSelection()); } _refreshLastCaretIfNeeded() { @@ -201,7 +204,7 @@ export default class BasicMessageEditor extends React.Component { this._lastSelection = cloneSelection(selection); const {caret, text} = getCaretOffsetAndText(this._editorRef, selection); this._lastCaret = caret; - this._lastTextLength = text.length; + this._isCaretAtEnd = caret.offset === text.length; } return this._lastCaret; } @@ -223,7 +226,7 @@ export default class BasicMessageEditor extends React.Component { } isCaretAtEnd() { - return this.getCaret().offset === this._lastTextLength; + return this._isCaretAtEnd; } _onBlur = () => { diff --git a/src/editor/position.js b/src/editor/position.js index 98b158e547..4693f62999 100644 --- a/src/editor/position.js +++ b/src/editor/position.js @@ -120,4 +120,17 @@ export default class DocumentPosition { const atEnd = offset >= lastPart.text.length; return new DocumentOffset(offset, atEnd); } + + isAtEnd(model) { + if (model.parts.length === 0) { + return true; + } + const lastPartIdx = model.parts.length - 1; + const lastPart = model.parts[lastPartIdx]; + return this.index === lastPartIdx && this.offset === lastPart.text.length; + } + + isAtStart() { + return this.index === 0 && this.offset === 0; + } } From 0252c7ae373a353f3092feb6e3e40e69c1b3da35 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 16:22:42 +0200 Subject: [PATCH 351/413] force pasting as plain text in new composer --- .../views/rooms/BasicMessageComposer.js | 15 ++++++++++++++ src/editor/deserialize.js | 2 +- src/editor/operations.js | 20 +++++++++++++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 36e142c0ea..fb5a10dee0 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -25,10 +25,12 @@ import { formatRangeAsQuote, formatRangeAsCode, formatInline, + replaceRangeAndMoveCaret, } from '../../../editor/operations'; import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; import Autocomplete from '../rooms/Autocomplete'; import {autoCompleteCreator} from '../../../editor/parts'; +import {parsePlainTextMessage} from '../../../editor/deserialize'; import {renderModel} from '../../../editor/render'; import {Room} from 'matrix-js-sdk'; import TypingStore from "../../../stores/TypingStore"; @@ -169,6 +171,18 @@ export default class BasicMessageEditor extends React.Component { this._onInput({inputType: "insertCompositionText"}); } + _onPaste = (event) => { + const {model} = this.props; + const {partCreator} = model; + const text = event.clipboardData.getData("text/plain"); + if (text) { + const range = getRangeForSelection(this._editorRef, model, document.getSelection()); + const parts = parsePlainTextMessage(text, partCreator); + replaceRangeAndMoveCaret(range, parts); + event.preventDefault(); + } + } + _onInput = (event) => { // ignore any input while doing IME compositions if (this._isIMEComposing) { @@ -492,6 +506,7 @@ export default class BasicMessageEditor extends React.Component { tabIndex="1" onBlur={this._onBlur} onFocus={this._onFocus} + onPaste={this._onPaste} onKeyDown={this._onKeyDown} ref={ref => this._editorRef = ref} aria-label={this.props.label} diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 08c66f592a..925d0d1ab3 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -232,7 +232,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) { return parts; } -function parsePlainTextMessage(body, partCreator, isQuotedMessage) { +export function parsePlainTextMessage(body, partCreator, isQuotedMessage) { const lines = body.split("\n"); const parts = lines.reduce((parts, line, i) => { if (isQuotedMessage) { diff --git a/src/editor/operations.js b/src/editor/operations.js index e6ed2ba0e5..4645e7d805 100644 --- a/src/editor/operations.js +++ b/src/editor/operations.js @@ -18,7 +18,8 @@ limitations under the License. * Some common queries and transformations on the editor model */ -export function replaceRangeAndExpandSelection(model, range, newParts) { +export function replaceRangeAndExpandSelection(range, newParts) { + const {model} = range; model.transform(() => { const oldLen = range.length; const addedLen = range.replace(newParts); @@ -28,6 +29,17 @@ export function replaceRangeAndExpandSelection(model, range, newParts) { }); } +export function replaceRangeAndMoveCaret(range, newParts) { + const {model} = range; + model.transform(() => { + const oldLen = range.length; + const addedLen = range.replace(newParts); + const firstOffset = range.start.asOffset(model); + const lastOffset = firstOffset.add(oldLen + addedLen); + return lastOffset.asPosition(model); + }); +} + export function rangeStartsAtBeginningOfLine(range) { const {model} = range; const startsWithPartial = range.start.offset !== 0; @@ -63,7 +75,7 @@ export function formatRangeAsQuote(range) { } parts.push(partCreator.newline()); - replaceRangeAndExpandSelection(model, range, parts); + replaceRangeAndExpandSelection(range, parts); } export function formatRangeAsCode(range) { @@ -85,7 +97,7 @@ export function formatRangeAsCode(range) { parts.unshift(partCreator.plain("`")); parts.push(partCreator.plain("`")); } - replaceRangeAndExpandSelection(model, range, parts); + replaceRangeAndExpandSelection(range, parts); } export function formatInline(range, prefix, suffix = prefix) { @@ -93,5 +105,5 @@ export function formatInline(range, prefix, suffix = prefix) { const {partCreator} = model; parts.unshift(partCreator.plain(prefix)); parts.push(partCreator.plain(suffix)); - replaceRangeAndExpandSelection(model, range, parts); + replaceRangeAndExpandSelection(range, parts); } From 9dac91a70d81a41db03fe3b3018cc8ec50739757 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Sep 2019 15:34:42 +0200 Subject: [PATCH 352/413] ensure step before formatting is persisted in undo history --- src/components/views/rooms/BasicMessageComposer.js | 1 + src/editor/history.js | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 36e142c0ea..8f4d05c446 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -437,6 +437,7 @@ export default class BasicMessageEditor extends React.Component { if (range.length === 0) { return; } + this.historyManager.ensureLastChangesPushed(this.props.model); switch (action) { case "bold": formatInline(range, "**"); diff --git a/src/editor/history.js b/src/editor/history.js index de052cf682..d66def4704 100644 --- a/src/editor/history.js +++ b/src/editor/history.js @@ -106,6 +106,12 @@ export default class HistoryManager { return shouldPush; } + ensureLastChangesPushed(model) { + if (this._changedSinceLastPush) { + this._pushState(model, this._lastCaret); + } + } + canUndo() { return this._currentIndex >= 1 || this._changedSinceLastPush; } @@ -117,9 +123,7 @@ export default class HistoryManager { // returns state that should be applied to model undo(model) { if (this.canUndo()) { - if (this._changedSinceLastPush) { - this._pushState(model, this._lastCaret); - } + this.ensureLastChangesPushed(model); this._currentIndex -= 1; return this._stack[this._currentIndex]; } From 2ff592c54259757aa4a2fcda8ad898543a4a019d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 10:48:24 +0100 Subject: [PATCH 353/413] Use existing modal promises --- src/components/views/settings/SetIdServer.js | 52 ++++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index e3d6d18c5b..ebd97cebc1 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -167,13 +167,14 @@ export default class SetIdServer extends React.Component { // Double check that the identity server even has terms of service. const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { - save &= await this._showNoTermsWarning(fullUrl); + const [confirmed] = await this._showNoTermsWarning(fullUrl); + save &= confirmed; } // Show a general warning, possibly with details about any bound // 3PIDs that would be left behind. - if (this.state.currentClientIdServer) { - save &= await this._showServerChangeWarning({ + if (save && this.state.currentClientIdServer) { + const [confirmed] = await this._showServerChangeWarning({ title: _t("Change identity server"), unboundMessage: _t( "Disconnect from the identity server and " + @@ -185,6 +186,7 @@ export default class SetIdServer extends React.Component { ), button: _t("Continue"), }); + save &= confirmed; } if (save) { @@ -209,29 +211,27 @@ export default class SetIdServer extends React.Component { _showNoTermsWarning(fullUrl) { const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); - return new Promise(resolve => { - Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { - title: _t("Identity server has no terms of service"), - description: ( -
- - {_t("The identity server you have chosen does not have any terms of service.")} - - -  {_t("Only continue if you trust the owner of the server.")} - -
- ), - button: _t("Continue"), - onFinished: resolve, - }); + const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { + title: _t("Identity server has no terms of service"), + description: ( +
+ + {_t("The identity server you have chosen does not have any terms of service.")} + + +  {_t("Only continue if you trust the owner of the server.")} + +
+ ), + button: _t("Continue"), }); + return finished; } _onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); try { - const confirmed = await this._showServerChangeWarning({ + const [confirmed] = await this._showServerChangeWarning({ title: _t("Disconnect identity server"), unboundMessage: _t( "Disconnect from the identity server ?", {}, @@ -268,14 +268,12 @@ export default class SetIdServer extends React.Component { } const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - return new Promise(resolve => { - Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, { - title, - description: message, - button, - onFinished: resolve, - }); + const { finished } = Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, { + title, + description: message, + button, }); + return finished; } _disconnectIdServer = () => { From dd1c01068f5c42fa4e8fbebe829fc5c54d855cf1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 6 Sep 2019 11:58:27 +0200 Subject: [PATCH 354/413] fix rename that didn't make it through rebasing --- src/components/views/rooms/BasicMessageComposer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 952bf452a4..2b4693646a 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -130,8 +130,8 @@ export default class BasicMessageEditor extends React.Component { } catch (err) { console.error(err); } - // if caret is a range, take the end position - const position = caret.end || caret; + // if caret selection is a range, take the end position + const position = selection.end || selection; this._setLastCaretFromPosition(position); } if (this.props.placeholder) { From 19fff51b01a1494dfca432d00ed3c0a91cdbad61 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 11:11:54 +0100 Subject: [PATCH 355/413] Rework handling for terms CORS error Earlier changes in this branch removed the "next step" of saving from the dialogs, so we need to fold in the CORS error case. --- src/components/views/settings/SetIdServer.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ebd97cebc1..20524d8ae3 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -165,7 +165,18 @@ export default class SetIdServer extends React.Component { let save = true; // Double check that the identity server even has terms of service. - const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); + let terms; + try { + terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); + } catch (e) { + console.error(e); + if (e.cors === "rejected" || e.httpStatus === 404) { + terms = null; + } else { + throw e; + } + } + if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { const [confirmed] = await this._showNoTermsWarning(fullUrl); save &= confirmed; @@ -194,10 +205,6 @@ export default class SetIdServer extends React.Component { } } catch (e) { console.error(e); - if (e.cors === "rejected" || e.httpStatus === 404) { - this._showNoTermsWarning(fullUrl); - return; - } errStr = _t("Terms of service not accepted or the identity server is invalid."); } } From df03477907dd7173a9fdf983ab592f9d2346f2a8 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 11:18:25 +0100 Subject: [PATCH 356/413] Show change warning only when different from current --- src/components/views/settings/SetIdServer.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 20524d8ae3..6460ad4b1f 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -147,10 +147,11 @@ export default class SetIdServer extends React.Component { _checkIdServer = async (e) => { e.preventDefault(); + const { idServer, currentClientIdServer } = this.state; this.setState({busy: true, checking: true, error: null}); - const fullUrl = unabbreviateUrl(this.state.idServer); + const fullUrl = unabbreviateUrl(idServer); let errStr = await checkIdentityServerUrl(fullUrl); if (!errStr) { @@ -179,25 +180,26 @@ export default class SetIdServer extends React.Component { if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { const [confirmed] = await this._showNoTermsWarning(fullUrl); - save &= confirmed; + save = confirmed; } // Show a general warning, possibly with details about any bound // 3PIDs that would be left behind. - if (save && this.state.currentClientIdServer) { + console.log(fullUrl, idServer, currentClientIdServer) + if (save && currentClientIdServer && fullUrl !== currentClientIdServer) { const [confirmed] = await this._showServerChangeWarning({ title: _t("Change identity server"), unboundMessage: _t( "Disconnect from the identity server and " + "connect to instead?", {}, { - current: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - new: sub => {abbreviateUrl(this.state.idServer)}, + current: sub => {abbreviateUrl(currentClientIdServer)}, + new: sub => {abbreviateUrl(idServer)}, }, ), button: _t("Continue"), }); - save &= confirmed; + save = confirmed; } if (save) { From 11a5fca7024b8f489c39aaebf773f6e07e931908 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 13:44:44 +0100 Subject: [PATCH 357/413] Remove logs --- src/components/views/settings/SetIdServer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 6460ad4b1f..d3fc944a70 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -185,7 +185,6 @@ export default class SetIdServer extends React.Component { // Show a general warning, possibly with details about any bound // 3PIDs that would be left behind. - console.log(fullUrl, idServer, currentClientIdServer) if (save && currentClientIdServer && fullUrl !== currentClientIdServer) { const [confirmed] = await this._showServerChangeWarning({ title: _t("Change identity server"), From 4db8ef4d89bd231b5131adc6afc5183a1dcffde0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Sep 2019 14:27:33 +0100 Subject: [PATCH 358/413] Correct case of propTypes property in ES6 React Components. React 16 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/IndicatorScrollbar.js | 2 +- .../views/context_menus/GenericElementContextMenu.js | 4 +--- src/components/views/context_menus/GenericTextContextMenu.js | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index d6efe8bee2..f14d99f730 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -19,7 +19,7 @@ import PropTypes from "prop-types"; import AutoHideScrollbar from "./AutoHideScrollbar"; export default class IndicatorScrollbar extends React.Component { - static PropTypes = { + static propTypes = { // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator // and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning // by the parent element. diff --git a/src/components/views/context_menus/GenericElementContextMenu.js b/src/components/views/context_menus/GenericElementContextMenu.js index 3f4804dbd1..cea684b663 100644 --- a/src/components/views/context_menus/GenericElementContextMenu.js +++ b/src/components/views/context_menus/GenericElementContextMenu.js @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; @@ -26,7 +24,7 @@ import PropTypes from 'prop-types'; export default class GenericElementContextMenu extends React.Component { - static PropTypes = { + static propTypes = { element: PropTypes.element.isRequired, // Function to be called when the parent window is resized // This can be used to reposition or close the menu on resize and diff --git a/src/components/views/context_menus/GenericTextContextMenu.js b/src/components/views/context_menus/GenericTextContextMenu.js index 2319fe05a2..068f83be5f 100644 --- a/src/components/views/context_menus/GenericTextContextMenu.js +++ b/src/components/views/context_menus/GenericTextContextMenu.js @@ -14,13 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; export default class GenericTextContextMenu extends React.Component { - static PropTypes = { + static propTypes = { message: PropTypes.string.isRequired, }; From 5fddb20d860ad51b2051fa3b12bae46d138d174b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 14:27:20 +0100 Subject: [PATCH 359/413] Stop setting IS input field on account change This stops setting a value in the IS input on account change. While it may have been marginally useful if you have the form open and change on a different device, it also seems to pick up changes on the current device, leading to strange UX locally. Fixes https://github.com/vector-im/riot-web/issues/10756 Fixes https://github.com/vector-im/riot-web/issues/10757 --- src/components/views/settings/SetIdServer.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index a718d87fa6..af38c997a3 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -93,18 +93,11 @@ export default class SetIdServer extends React.Component { onAction = (payload) => { // We react to changes in the ID server in the event the user is staring at this form - // when changing their identity server on another device. If the user is trying to change - // it in two places, we'll end up stomping all over their input, but at that point we - // should question our UX which led to them doing that. + // when changing their identity server on another device. if (payload.action !== "id_server_changed") return; - const fullUrl = MatrixClientPeg.get().getIdentityServerUrl(); - let abbr = ''; - if (fullUrl) abbr = abbreviateUrl(fullUrl); - this.setState({ - currentClientIdServer: fullUrl, - idServer: abbr, + currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), }); }; From 4e98721ba909fb17dcd07d24aff7ebab7b8f59c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 6 Sep 2019 15:56:46 +0200 Subject: [PATCH 360/413] take bounding box for positioning calculation --- .../views/rooms/MessageComposerFormatBar.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.js index af7bb70e70..8090fb2ad5 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.js +++ b/src/components/views/rooms/MessageComposerFormatBar.js @@ -38,23 +38,10 @@ export default class MessageComposerFormatBar extends React.PureComponent { showAt(selectionRect) { this._formatBarRef.classList.add("mx_MessageComposerFormatBar_shown"); - let leftOffset = 0; - let node = this._formatBarRef; - while (node.offsetParent) { - node = node.offsetParent; - leftOffset += node.offsetLeft; - } - - let topOffset = 0; - node = this._formatBarRef; - while (node.offsetParent) { - node = node.offsetParent; - topOffset += node.offsetTop; - } - - this._formatBarRef.style.left = `${selectionRect.left - leftOffset}px`; + const parentRect = this._formatBarRef.parentElement.getBoundingClientRect(); + this._formatBarRef.style.left = `${selectionRect.left - parentRect.left}px`; // 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok. - this._formatBarRef.style.top = `${selectionRect.top - topOffset - 16 - 12}px`; + this._formatBarRef.style.top = `${selectionRect.top - parentRect.top - 16 - 12}px`; } hide() { From 70ff2bc9cd9afb36121830bf3c3628b6a624b337 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Sep 2019 15:04:46 +0100 Subject: [PATCH 361/413] Switch to createReactClass: views/rooms and test/components. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/AppsDrawer.js | 5 ++--- src/components/views/rooms/AuxPanel.js | 3 ++- src/components/views/rooms/EntityTile.js | 9 ++++----- src/components/views/rooms/EventTile.js | 7 +++---- src/components/views/rooms/ForwardMessage.js | 3 ++- src/components/views/rooms/LinkPreviewWidget.js | 5 ++--- src/components/views/rooms/MemberInfo.js | 3 ++- src/components/views/rooms/MemberList.js | 3 ++- src/components/views/rooms/MemberTile.js | 7 +++---- src/components/views/rooms/PinnedEventTile.js | 3 ++- src/components/views/rooms/PinnedEventsPanel.js | 3 ++- src/components/views/rooms/PresenceLabel.js | 5 ++--- src/components/views/rooms/ReadReceiptMarker.js | 9 ++++----- src/components/views/rooms/RoomDetailList.js | 3 ++- src/components/views/rooms/RoomDetailRow.js | 3 ++- src/components/views/rooms/RoomDropTarget.js | 7 +++---- src/components/views/rooms/RoomHeader.js | 5 ++--- src/components/views/rooms/RoomList.js | 8 ++++---- src/components/views/rooms/RoomNameEditor.js | 7 +++---- src/components/views/rooms/RoomPreviewBar.js | 5 ++--- src/components/views/rooms/RoomTile.js | 3 ++- src/components/views/rooms/RoomTopicEditor.js | 9 ++++----- src/components/views/rooms/RoomUpgradeWarningBar.js | 3 ++- src/components/views/rooms/SearchBar.js | 9 +++------ src/components/views/rooms/SearchResultTile.js | 9 ++++----- src/components/views/rooms/SearchableEntityList.js | 10 +++++----- src/components/views/rooms/SimpleRoomHeader.js | 3 ++- src/components/views/rooms/TopUnreadMessagesBar.js | 10 +++------- src/components/views/rooms/UserTile.js | 10 +++------- src/components/views/rooms/WhoIsTypingTile.js | 3 ++- test/components/structures/MessagePanel-test.js | 7 ++++--- test/components/stub-component.js | 6 +++--- 32 files changed, 87 insertions(+), 98 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 19a5a6c468..2a0a7569fb 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -15,10 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import MatrixClientPeg from '../../../MatrixClientPeg'; import AppTile from '../elements/AppTile'; import Modal from '../../../Modal'; @@ -35,7 +34,7 @@ import SettingsStore from "../../../settings/SettingsStore"; // The maximum number of widgets that can be added in a room const MAX_WIDGETS = 2; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'AppsDrawer', propTypes: { diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index 6cbec30c4d..ffb5d9272d 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import MatrixClientPeg from "../../../MatrixClientPeg"; import sdk from '../../../index'; import dis from "../../../dispatcher"; @@ -28,7 +29,7 @@ import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'AuxPanel', propTypes: { diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index bfeeced339..0193275ca0 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -15,11 +15,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; -const sdk = require('../../../index'); +import createReactClass from 'create-react-class'; +import sdk from '../../../index'; import AccessibleButton from '../elements/AccessibleButton'; import { _t } from '../../../languageHandler'; import classNames from "classnames"; @@ -52,7 +51,7 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { } } -const EntityTile = React.createClass({ +const EntityTile = createReactClass({ displayName: 'EntityTile', propTypes: { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 5fec115c95..5152ffde3e 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -17,12 +17,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import ReplyThread from "../elements/ReplyThread"; -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; const classNames = require("classnames"); import { _t, _td } from '../../../languageHandler'; const Modal = require('../../../Modal'); @@ -84,7 +83,7 @@ const MAX_READ_AVATARS = 5; // | '--------------------------------------' | // '----------------------------------------------------------' -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'EventTile', propTypes: { diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js index 11ca7487cc..4a6c560d2c 100644 --- a/src/components/views/rooms/ForwardMessage.js +++ b/src/components/views/rooms/ForwardMessage.js @@ -17,12 +17,13 @@ import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher'; import { KeyCode } from '../../../Keyboard'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ForwardMessage', propTypes: { diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 8b6f295080..cfa096a763 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { linkifyElement } from '../../../HtmlUtils'; const sdk = require('../../../index'); @@ -25,7 +24,7 @@ const MatrixClientPeg = require('../../../MatrixClientPeg'); const ImageUtils = require('../../../ImageUtils'); const Modal = require('../../../Modal'); -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'LinkPreviewWidget', propTypes: { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index bb8fb7944f..26f731d308 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -29,6 +29,7 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import classNames from 'classnames'; import { MatrixClient } from 'matrix-js-sdk'; import dis from '../../../dispatcher'; @@ -48,7 +49,7 @@ import E2EIcon from "./E2EIcon"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import MatrixClientPeg from "../../../MatrixClientPeg"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MemberInfo', propTypes: { diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 1e7bf6d1b2..1ecb04d442 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -17,6 +17,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher'; @@ -31,7 +32,7 @@ const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; const SHOW_MORE_INCREMENT = 100; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MemberList', getInitialState: function() { diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 7303a4e34f..c002849450 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -14,18 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import SettingsStore from "../../../settings/SettingsStore"; -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; const sdk = require('../../../index'); const dis = require('../../../dispatcher'); import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MemberTile', propTypes: { diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js index 8b2b217e19..cc086f66da 100644 --- a/src/components/views/rooms/PinnedEventTile.js +++ b/src/components/views/rooms/PinnedEventTile.js @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import MatrixClientPeg from "../../../MatrixClientPeg"; import dis from "../../../dispatcher"; import AccessibleButton from "../elements/AccessibleButton"; @@ -24,7 +25,7 @@ import MemberAvatar from "../avatars/MemberAvatar"; import { _t } from '../../../languageHandler'; import {formatFullDate} from '../../../DateUtils'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'PinnedEventTile', propTypes: { mxRoom: PropTypes.object.isRequired, diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js index 3a0bc0e326..dd2febdf39 100644 --- a/src/components/views/rooms/PinnedEventsPanel.js +++ b/src/components/views/rooms/PinnedEventsPanel.js @@ -16,13 +16,14 @@ limitations under the License. import React from "react"; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import MatrixClientPeg from "../../../MatrixClientPeg"; import AccessibleButton from "../elements/AccessibleButton"; import PinnedEventTile from "./PinnedEventTile"; import { _t } from '../../../languageHandler'; import PinningUtils from "../../../utils/PinningUtils"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'PinnedEventsPanel', propTypes: { // The Room from the js-sdk we're going to show pinned events for diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js index 22a597b42b..5cb34b473f 100644 --- a/src/components/views/rooms/PresenceLabel.js +++ b/src/components/views/rooms/PresenceLabel.js @@ -14,15 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'PresenceLabel', propTypes: { diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 2f7a599d95..27c5e8c20e 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); -const ReactDOM = require('react-dom'); +import React from 'react'; +import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; const sdk = require('../../../index'); @@ -36,7 +35,7 @@ try { } catch (e) { } -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ReadReceiptMarker', propTypes: { diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.js index 11fad41b39..9ec70dc8e0 100644 --- a/src/components/views/rooms/RoomDetailList.js +++ b/src/components/views/rooms/RoomDetailList.js @@ -19,11 +19,12 @@ import dis from '../../../dispatcher'; import React from 'react'; import { _t } from '../../../languageHandler'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import classNames from 'classnames'; import {roomShape} from './RoomDetailRow'; -export default React.createClass({ +export default createReactClass({ displayName: 'RoomDetailList', propTypes: { diff --git a/src/components/views/rooms/RoomDetailRow.js b/src/components/views/rooms/RoomDetailRow.js index 09d7eb22ed..ec6bb64bf0 100644 --- a/src/components/views/rooms/RoomDetailRow.js +++ b/src/components/views/rooms/RoomDetailRow.js @@ -21,6 +21,7 @@ import { linkifyElement } from '../../../HtmlUtils'; import { ContentRepo } from 'matrix-js-sdk'; import MatrixClientPeg from '../../../MatrixClientPeg'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; export function getDisplayAliasForRoom(room) { return room.canonicalAlias || (room.aliases ? room.aliases[0] : ""); @@ -39,7 +40,7 @@ export const roomShape = PropTypes.shape({ guestCanJoin: PropTypes.bool, }); -export default React.createClass({ +export default createReactClass({ propTypes: { room: roomShape, // passes ev, room as args diff --git a/src/components/views/rooms/RoomDropTarget.js b/src/components/views/rooms/RoomDropTarget.js index 13050cf860..1012b23105 100644 --- a/src/components/views/rooms/RoomDropTarget.js +++ b/src/components/views/rooms/RoomDropTarget.js @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +import React from 'react'; +import createReactClass from 'create-react-class'; -const React = require('react'); - -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomDropTarget', render: function() { diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index a40746dd04..5b6c0f6d2d 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import classNames from 'classnames'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -33,7 +32,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import E2EIcon from './E2EIcon'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomHeader', propTypes: { diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index ef7d0ed5fb..2454f012f8 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -15,12 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; import SettingsStore from "../../../settings/SettingsStore"; import Timer from "../../../utils/Timer"; -const React = require("react"); -const ReactDOM = require("react-dom"); +import React from "react"; +import ReactDOM from "react-dom"; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; const MatrixClientPeg = require("../../../MatrixClientPeg"); @@ -64,7 +64,7 @@ function phraseForSection(section) { } } -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomList', propTypes: { diff --git a/src/components/views/rooms/RoomNameEditor.js b/src/components/views/rooms/RoomNameEditor.js index d073a0be25..5bdf719e97 100644 --- a/src/components/views/rooms/RoomNameEditor.js +++ b/src/components/views/rooms/RoomNameEditor.js @@ -14,15 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; const sdk = require('../../../index'); const MatrixClientPeg = require('../../../MatrixClientPeg'); import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomNameEditor', propTypes: { diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index ccd4559586..513b867d4f 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -16,10 +16,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; @@ -44,7 +43,7 @@ const MessageCase = Object.freeze({ OtherError: "OtherError", }); -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomPreviewBar', propTypes: { diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index be73985d16..2ec5384b93 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -20,6 +20,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import classNames from 'classnames'; import dis from '../../../dispatcher'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -33,7 +34,7 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver'; import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomTile', propTypes: { diff --git a/src/components/views/rooms/RoomTopicEditor.js b/src/components/views/rooms/RoomTopicEditor.js index 7ad02f264c..a7d11313ff 100644 --- a/src/components/views/rooms/RoomTopicEditor.js +++ b/src/components/views/rooms/RoomTopicEditor.js @@ -14,14 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; -const sdk = require('../../../index'); +import createReactClass from 'create-react-class'; +import sdk from '../../../index'; import { _t } from "../../../languageHandler"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomTopicEditor', propTypes: { diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js index edde0a6865..58d959ddcc 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.js +++ b/src/components/views/rooms/RoomUpgradeWarningBar.js @@ -16,13 +16,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from "../../../MatrixClientPeg"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomUpgradeWarningBar', propTypes: { diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.js index fb9c3103ab..5e0ac9a953 100644 --- a/src/components/views/rooms/SearchBar.js +++ b/src/components/views/rooms/SearchBar.js @@ -14,16 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); -const MatrixClientPeg = require('../../../MatrixClientPeg'); -const sdk = require('../../../index'); +import React from 'react'; +import createReactClass from 'create-react-class'; const classNames = require('classnames'); const AccessibleButton = require('../../../components/views/elements/AccessibleButton'); import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'SearchBar', getInitialState: function() { diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js index e396ea7011..19ed490683 100644 --- a/src/components/views/rooms/SearchResultTile.js +++ b/src/components/views/rooms/SearchResultTile.js @@ -14,13 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; -const sdk = require('../../../index'); +import createReactClass from 'create-react-class'; +import sdk from '../../../index'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'SearchResult', propTypes: { diff --git a/src/components/views/rooms/SearchableEntityList.js b/src/components/views/rooms/SearchableEntityList.js index 876bb155bc..024816c6fc 100644 --- a/src/components/views/rooms/SearchableEntityList.js +++ b/src/components/views/rooms/SearchableEntityList.js @@ -13,16 +13,16 @@ 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. */ -const React = require('react'); + +import React from 'react'; import PropTypes from 'prop-types'; -const MatrixClientPeg = require("../../../MatrixClientPeg"); -const Modal = require("../../../Modal"); -const sdk = require("../../../index"); +import createReactClass from 'create-react-class'; +import sdk from "../../../index"; import { _t } from '../../../languageHandler'; // A list capable of displaying entities which conform to the SearchableEntity // interface which is an object containing getJsx(): Jsx and matches(query: string): boolean -const SearchableEntityList = React.createClass({ +const SearchableEntityList = createReactClass({ displayName: 'SearchableEntityList', propTypes: { diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 4ced9fb351..e1ade691d2 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import AccessibleButton from '../elements/AccessibleButton'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -36,7 +37,7 @@ export function CancelButton(props) { * A stripped-down room header used for things like the user settings * and room directory. */ -export default React.createClass({ +export default createReactClass({ displayName: 'SimpleRoomHeader', propTypes: { diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.js index 99f3b7fb23..c7a1a22580 100644 --- a/src/components/views/rooms/TopUnreadMessagesBar.js +++ b/src/components/views/rooms/TopUnreadMessagesBar.js @@ -16,17 +16,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; -import {formatCount} from '../../../utils/FormattingUtils'; -const sdk = require('../../../index'); - -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'TopUnreadMessagesBar', propTypes: { diff --git a/src/components/views/rooms/UserTile.js b/src/components/views/rooms/UserTile.js index 07a9c00917..76afda6dd7 100644 --- a/src/components/views/rooms/UserTile.js +++ b/src/components/views/rooms/UserTile.js @@ -14,18 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; const Avatar = require("../../../Avatar"); -const MatrixClientPeg = require('../../../MatrixClientPeg'); const sdk = require('../../../index'); -const dis = require('../../../dispatcher'); -const Modal = require("../../../Modal"); -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'UserTile', propTypes: { diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index c639b205d8..0e23286eb6 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -17,12 +17,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import WhoIsTyping from '../../../WhoIsTyping'; import Timer from '../../../utils/Timer'; import MatrixClientPeg from '../../../MatrixClientPeg'; import MemberAvatar from '../avatars/MemberAvatar'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'WhoIsTypingTile', propTypes: { diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 58b1590cf1..f58f1b925c 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -16,8 +16,9 @@ limitations under the License. import SettingsStore from "../../../src/settings/SettingsStore"; -const React = require('react'); -const ReactDOM = require("react-dom"); +import React from 'react'; +import createReactClass from 'create-react-class'; +import ReactDOM from "react-dom"; import PropTypes from "prop-types"; const TestUtils = require('react-dom/test-utils'); const expect = require('expect'); @@ -39,7 +40,7 @@ let client; const room = new Matrix.Room(); // wrap MessagePanel with a component which provides the MatrixClient in the context. -const WrappedMessagePanel = React.createClass({ +const WrappedMessagePanel = createReactClass({ childContextTypes: { matrixClient: PropTypes.object, room: PropTypes.object, diff --git a/test/components/stub-component.js b/test/components/stub-component.js index 308b183719..9264792ffb 100644 --- a/test/components/stub-component.js +++ b/test/components/stub-component.js @@ -1,8 +1,8 @@ /* A dummy React component which we use for stubbing out app-level components */ -'use strict'; -const React = require('react'); +import React from 'react'; +import createReactClass from 'create-react-class'; module.exports = function(opts) { opts = opts || {}; @@ -16,5 +16,5 @@ module.exports = function(opts) { }; } - return React.createClass(opts); + return createReactClass(opts); }; From 3ad88604cc32e95a92a43af054790a8c2d6408d9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 6 Sep 2019 13:43:21 +0100 Subject: [PATCH 362/413] Stregthen bound 3PID warning dialog This tweaks the bound 3PID text and adds danger styling. Fixes https://github.com/vector-im/riot-web/issues/10750 --- src/components/views/settings/SetIdServer.js | 16 +++++++++++----- src/i18n/strings/en_EN.json | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index d3fc944a70..afb2d62e0e 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -260,17 +260,21 @@ export default class SetIdServer extends React.Component { const boundThreepids = threepids.filter(tp => tp.bound); let message; + let danger = false; if (boundThreepids.length) { message = _t( - "You are currently sharing email addresses or phone numbers on the identity " + - "server . You will need to reconnect to to stop " + - "sharing them.", {}, + "You are still sharing your personal data on the identity " + + "server .

" + + "We recommend that you remove your email addresses and phone numbers " + + "from the identity server before disconnecting.", {}, { idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - // XXX: https://github.com/vector-im/riot-web/issues/9086 - idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + b: sub => {sub}, + br: () =>
, }, ); + danger = true; + button = _t("Disconnect anyway"); } else { message = unboundMessage; } @@ -280,6 +284,8 @@ export default class SetIdServer extends React.Component { title, description: message, button, + cancelButton: _t("Go back"), + danger, }); return finished; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f31fcc7157..02fd05b067 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -563,7 +563,9 @@ "Disconnect identity server": "Disconnect identity server", "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect": "Disconnect", - "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", + "You are still sharing your personal data on the identity server .

We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "You are still sharing your personal data on the identity server .

We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", + "Disconnect anyway": "Disconnect anyway", + "Go back": "Go back", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.", @@ -1287,7 +1289,6 @@ "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback", - "Go back": "Go back", "Room Settings - %(roomName)s": "Room Settings - %(roomName)s", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", From 26bd694c6a4a78376621927562c87767fe5843f8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 6 Sep 2019 16:25:06 +0200 Subject: [PATCH 363/413] support toggling inline formatting --- .../views/rooms/BasicMessageComposer.js | 8 +++---- src/editor/operations.js | 23 ++++++++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 2b4693646a..99e7b8ea07 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -24,7 +24,7 @@ import {setSelection} from '../../../editor/caret'; import { formatRangeAsQuote, formatRangeAsCode, - formatInline, + toggleInlineFormat, replaceRangeAndMoveCaret, } from '../../../editor/operations'; import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; @@ -457,13 +457,13 @@ export default class BasicMessageEditor extends React.Component { this.historyManager.ensureLastChangesPushed(this.props.model); switch (action) { case "bold": - formatInline(range, "**"); + toggleInlineFormat(range, "**"); break; case "italics": - formatInline(range, "*"); + toggleInlineFormat(range, "*"); break; case "strikethrough": - formatInline(range, "", ""); + toggleInlineFormat(range, "", ""); break; case "code": formatRangeAsCode(range); diff --git a/src/editor/operations.js b/src/editor/operations.js index 4645e7d805..e2661faf59 100644 --- a/src/editor/operations.js +++ b/src/editor/operations.js @@ -100,10 +100,27 @@ export function formatRangeAsCode(range) { replaceRangeAndExpandSelection(range, parts); } -export function formatInline(range, prefix, suffix = prefix) { +export function toggleInlineFormat(range, prefix, suffix = prefix) { const {model, parts} = range; const {partCreator} = model; - parts.unshift(partCreator.plain(prefix)); - parts.push(partCreator.plain(suffix)); + + const isFormatted = parts.length && + parts[0].text.startsWith(prefix) && + parts[parts.length - 1].text.endsWith(suffix); + + if (isFormatted) { + // remove prefix and suffix + const partWithoutPrefix = parts[0].serialize(); + partWithoutPrefix.text = partWithoutPrefix.text.substr(prefix.length); + parts[0] = partCreator.deserializePart(partWithoutPrefix); + + const partWithoutSuffix = parts[parts.length - 1].serialize(); + const suffixPartText = partWithoutSuffix.text; + partWithoutSuffix.text = suffixPartText.substring(0, suffixPartText.length - suffix.length); + parts[parts.length - 1] = partCreator.deserializePart(partWithoutSuffix); + } else { + parts.unshift(partCreator.plain(prefix)); + parts.push(partCreator.plain(suffix)); + } replaceRangeAndExpandSelection(range, parts); } From 48247e66be4fe83e972d52fef9e8c5aed5253986 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 6 Sep 2019 17:50:23 +0200 Subject: [PATCH 364/413] use underscore for italics so it doesn't collide with bold when toggling --- src/components/views/rooms/BasicMessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 99e7b8ea07..eabb97a6f5 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -460,7 +460,7 @@ export default class BasicMessageEditor extends React.Component { toggleInlineFormat(range, "**"); break; case "italics": - toggleInlineFormat(range, "*"); + toggleInlineFormat(range, "_"); break; case "strikethrough": toggleInlineFormat(range, "", ""); From d5db67be38c57ce821108a0a27ab98b55ec7a367 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Sep 2019 18:35:52 +0100 Subject: [PATCH 365/413] Switch to createReactClass: views/elements & views/groups. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/ActionButton.js | 3 ++- src/components/views/elements/AddressSelector.js | 5 ++--- src/components/views/elements/AddressTile.js | 3 ++- src/components/views/elements/DeviceVerifyButtons.js | 3 ++- src/components/views/elements/DialogButtons.js | 3 ++- src/components/views/elements/EditableText.js | 3 ++- src/components/views/elements/InlineSpinner.js | 5 +++-- .../views/elements/MemberEventListSummary.js | 3 ++- src/components/views/elements/MessageSpinner.js | 3 ++- src/components/views/elements/PersistentApp.js | 3 ++- src/components/views/elements/Pill.js | 3 ++- src/components/views/elements/PowerSelector.js | 5 ++--- src/components/views/elements/ProgressBar.js | 7 +++---- src/components/views/elements/SettingsFlag.js | 3 ++- src/components/views/elements/Spinner.js | 7 +++---- src/components/views/elements/TagTile.js | 3 ++- src/components/views/elements/TintableSvg.js | 10 ++++------ src/components/views/elements/Tooltip.js | 3 ++- src/components/views/elements/TooltipButton.js | 3 ++- src/components/views/elements/TruncatedList.js | 3 ++- src/components/views/elements/UserSelector.js | 5 ++--- src/components/views/groups/GroupInviteTile.js | 3 ++- src/components/views/groups/GroupMemberInfo.js | 5 +++-- src/components/views/groups/GroupMemberList.js | 3 ++- src/components/views/groups/GroupMemberTile.js | 3 ++- src/components/views/groups/GroupPublicityToggle.js | 3 ++- src/components/views/groups/GroupRoomInfo.js | 5 +++-- src/components/views/groups/GroupRoomList.js | 3 ++- src/components/views/groups/GroupRoomTile.js | 5 +++-- src/components/views/groups/GroupTile.js | 3 ++- src/components/views/groups/GroupUserSettings.js | 3 ++- 31 files changed, 70 insertions(+), 52 deletions(-) diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index 1eb082a917..ea9a9bd876 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -16,12 +16,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import AccessibleButton from './AccessibleButton'; import dis from '../../../dispatcher'; import sdk from '../../../index'; import Analytics from '../../../Analytics'; -export default React.createClass({ +export default createReactClass({ displayName: 'RoleButton', propTypes: { diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index 23e6939a24..fad57890c4 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -15,15 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import classNames from 'classnames'; import { UserAddressType } from '../../../UserAddress'; -export default React.createClass({ +export default createReactClass({ displayName: 'AddressSelector', propTypes: { diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 8011a6c55f..6d6ac20a5d 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import classNames from 'classnames'; import sdk from "../../../index"; import MatrixClientPeg from "../../../MatrixClientPeg"; @@ -24,7 +25,7 @@ import { _t } from '../../../languageHandler'; import { UserAddressType } from '../../../UserAddress.js'; -export default React.createClass({ +export default createReactClass({ displayName: 'AddressTile', propTypes: { diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index f0be1f4bf2..15678b7d7a 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -16,12 +16,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'DeviceVerifyButtons', propTypes: { diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js index 70355b56b7..e7b3a9c7eb 100644 --- a/src/components/views/elements/DialogButtons.js +++ b/src/components/views/elements/DialogButtons.js @@ -17,12 +17,13 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; /** * Basic container for buttons in modal dialogs. */ -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: "DialogButtons", propTypes: { diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 44f86f1be8..0f5c4a2192 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -17,8 +17,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'EditableText', propTypes: { diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js index f82f309493..18711f90d3 100644 --- a/src/components/views/elements/InlineSpinner.js +++ b/src/components/views/elements/InlineSpinner.js @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -const React = require('react'); +import React from "react"; +import createReactClass from 'create-react-class'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'InlineSpinner', render: function() { diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 6d8b490d98..ba31eb5a38 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -18,11 +18,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MemberEventListSummary', propTypes: { diff --git a/src/components/views/elements/MessageSpinner.js b/src/components/views/elements/MessageSpinner.js index 19d804f511..f00fdcf576 100644 --- a/src/components/views/elements/MessageSpinner.js +++ b/src/components/views/elements/MessageSpinner.js @@ -15,8 +15,9 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MessageSpinner', render: function() { diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index facf5d1179..d6931850be 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import RoomViewStore from '../../../stores/RoomViewStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import WidgetUtils from '../../../utils/WidgetUtils'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'PersistentApp', getInitialState: function() { diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 9d9e5a9e79..4c987a0095 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import classNames from 'classnames'; @@ -31,7 +32,7 @@ const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); // HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`) const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/; -const Pill = React.createClass({ +const Pill = createReactClass({ statics: { isPillUrl: (url) => { return !!REGEX_MATRIXTO.exec(url); diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 4089c4dd86..c56a5d8502 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -14,15 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import * as Roles from '../../../Roles'; import { _t } from '../../../languageHandler'; import Field from "./Field"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'PowerSelector', propTypes: { diff --git a/src/components/views/elements/ProgressBar.js b/src/components/views/elements/ProgressBar.js index 15da5d44f6..3561763e51 100644 --- a/src/components/views/elements/ProgressBar.js +++ b/src/components/views/elements/ProgressBar.js @@ -14,12 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from "react"; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ProgressBar', propTypes: { value: PropTypes.number, diff --git a/src/components/views/elements/SettingsFlag.js b/src/components/views/elements/SettingsFlag.js index f1bd72f53d..a26646b08c 100644 --- a/src/components/views/elements/SettingsFlag.js +++ b/src/components/views/elements/SettingsFlag.js @@ -16,11 +16,12 @@ limitations under the License. import React from "react"; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import SettingsStore from "../../../settings/SettingsStore"; import { _t } from '../../../languageHandler'; import ToggleSwitch from "./ToggleSwitch"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'SettingsFlag', propTypes: { name: PropTypes.string.isRequired, diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js index ca1ee0ef42..5d43e836cc 100644 --- a/src/components/views/elements/Spinner.js +++ b/src/components/views/elements/Spinner.js @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +import React from "react"; +import createReactClass from 'create-react-class'; -const React = require('react'); - -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'Spinner', render: function() { diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js index ef9864358b..8a3b85b65a 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import classNames from 'classnames'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; @@ -34,7 +35,7 @@ import TagOrderStore from '../../../stores/TagOrderStore'; // - Rooms that are part of the group // - Direct messages with members of the group // with the intention that this could be expanded to arbitrary tags in future. -export default React.createClass({ +export default createReactClass({ displayName: 'TagTile', propTypes: { diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index e04bf87793..73ba375d59 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -14,14 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); -const ReactDOM = require("react-dom"); +import React from 'react'; import PropTypes from 'prop-types'; -const Tinter = require("../../../Tinter"); +import createReactClass from 'create-react-class'; +import Tinter from "../../../Tinter"; -var TintableSvg = React.createClass({ +const TintableSvg = createReactClass({ displayName: 'TintableSvg', propTypes: { diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.js index 27de392a44..bb5f9f0604 100644 --- a/src/components/views/elements/Tooltip.js +++ b/src/components/views/elements/Tooltip.js @@ -20,12 +20,13 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import dis from '../../../dispatcher'; import classNames from 'classnames'; const MIN_TOOLTIP_HEIGHT = 25; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'Tooltip', propTypes: { diff --git a/src/components/views/elements/TooltipButton.js b/src/components/views/elements/TooltipButton.js index 63cf3fe1fe..0cabf776a4 100644 --- a/src/components/views/elements/TooltipButton.js +++ b/src/components/views/elements/TooltipButton.js @@ -16,9 +16,10 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'TooltipButton', getInitialState: function() { diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.js index 1a674eef65..e6a5e2ae32 100644 --- a/src/components/views/elements/TruncatedList.js +++ b/src/components/views/elements/TruncatedList.js @@ -17,9 +17,10 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'TruncatedList', propTypes: { diff --git a/src/components/views/elements/UserSelector.js b/src/components/views/elements/UserSelector.js index 572f8488bc..a01e3584a0 100644 --- a/src/components/views/elements/UserSelector.js +++ b/src/components/views/elements/UserSelector.js @@ -14,13 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'UserSelector', propTypes: { diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index 843bb29055..7d7275c55b 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; @@ -25,7 +26,7 @@ import classNames from 'classnames'; import MatrixClientPeg from "../../../MatrixClientPeg"; import {createMenu} from "../../structures/ContextualMenu"; -export default React.createClass({ +export default createReactClass({ displayName: 'GroupInviteTile', propTypes: { diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 34a7e139fd..75e647aa4b 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -15,8 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import PropTypes from 'prop-types'; import React from 'react'; +import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; @@ -26,7 +27,7 @@ import { GroupMemberType } from '../../../groups'; import GroupStore from '../../../stores/GroupStore'; import AccessibleButton from '../elements/AccessibleButton'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'GroupMemberInfo', contextTypes: { diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 9045c92a2e..d13f54579d 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import dis from '../../../dispatcher'; @@ -27,7 +28,7 @@ import RightPanel from '../../structures/RightPanel'; const INITIAL_LOAD_NUM_MEMBERS = 30; -export default React.createClass({ +export default createReactClass({ displayName: 'GroupMemberList', propTypes: { diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js index 971255d548..ed305382be 100644 --- a/src/components/views/groups/GroupMemberTile.js +++ b/src/components/views/groups/GroupMemberTile.js @@ -18,12 +18,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { GroupMemberType } from '../../../groups'; -export default React.createClass({ +export default createReactClass({ displayName: 'GroupMemberTile', propTypes: { diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js index 98fa598e20..bacf54382a 100644 --- a/src/components/views/groups/GroupPublicityToggle.js +++ b/src/components/views/groups/GroupPublicityToggle.js @@ -16,11 +16,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import GroupStore from '../../../stores/GroupStore'; import ToggleSwitch from "../elements/ToggleSwitch"; -export default React.createClass({ +export default createReactClass({ displayName: 'GroupPublicityToggle', propTypes: { diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index 7296b25344..c6d07cee50 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import PropTypes from 'prop-types'; import React from 'react'; +import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; @@ -23,7 +24,7 @@ import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import GroupStore from '../../../stores/GroupStore'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'GroupRoomInfo', contextTypes: { diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js index ec41cd036b..81921568d0 100644 --- a/src/components/views/groups/GroupRoomList.js +++ b/src/components/views/groups/GroupRoomList.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import GroupStore from '../../../stores/GroupStore'; @@ -24,7 +25,7 @@ import TintableSvg from '../elements/TintableSvg'; const INITIAL_LOAD_NUM_ROOMS = 30; -export default React.createClass({ +export default createReactClass({ propTypes: { groupId: PropTypes.string.isRequired, }, diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index a4961fefa9..ae325d4796 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; -import {MatrixClient} from 'matrix-js-sdk'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; +import {MatrixClient} from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { GroupRoomType } from '../../../groups'; -const GroupRoomTile = React.createClass({ +const GroupRoomTile = createReactClass({ displayName: 'GroupRoomTile', propTypes: { diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index 18ef5a5637..3b64c10a1e 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import {MatrixClient} from 'matrix-js-sdk'; import { Draggable, Droppable } from 'react-beautiful-dnd'; import sdk from '../../../index'; @@ -24,7 +25,7 @@ import FlairStore from '../../../stores/FlairStore'; function nop() {} -const GroupTile = React.createClass({ +const GroupTile = createReactClass({ displayName: 'GroupTile', propTypes: { diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 210fca404a..7d80bdd209 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -16,11 +16,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import { MatrixClient } from 'matrix-js-sdk'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'GroupUserSettings', contextTypes: { From b243004a6cc5611a733cc2e40b7620737bfaf081 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Sep 2019 18:37:43 +0100 Subject: [PATCH 366/413] Switch to createReactClass: *everything else*. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/avatars/BaseAvatar.js | 3 ++- src/components/views/avatars/GroupAvatar.js | 3 ++- src/components/views/avatars/MemberAvatar.js | 7 +++---- src/components/views/avatars/RoomAvatar.js | 3 ++- .../views/context_menus/MessageContextMenu.js | 3 ++- .../views/context_menus/RoomTileContextMenu.js | 5 +++-- src/components/views/create_room/CreateRoomButton.js | 6 +++--- src/components/views/create_room/Presets.js | 7 +++---- src/components/views/create_room/RoomAlias.js | 5 +++-- src/components/views/globals/MatrixToolbar.js | 5 ++--- src/components/views/globals/NewVersionBar.js | 3 ++- src/components/views/globals/PasswordNagBar.js | 3 ++- src/components/views/globals/ServerLimitBar.js | 3 ++- src/components/views/globals/UpdateCheckBar.js | 3 ++- src/components/views/messages/MFileBody.js | 5 ++--- src/components/views/messages/MVideoBody.js | 5 ++--- src/components/views/messages/MessageEvent.js | 9 ++++----- src/components/views/messages/RoomAvatarEvent.js | 3 ++- src/components/views/messages/RoomCreate.js | 3 ++- src/components/views/messages/SenderProfile.js | 5 ++--- src/components/views/messages/TextualBody.js | 6 ++---- src/components/views/messages/TextualEvent.js | 7 +++---- src/components/views/messages/UnknownBody.js | 5 ++--- src/components/views/room_settings/ColorSettings.js | 12 +++++------- .../views/room_settings/UrlPreviewSettings.js | 7 ++++--- src/components/views/settings/ChangeAvatar.js | 9 +++++---- src/components/views/settings/ChangeDisplayName.js | 3 ++- src/components/views/settings/ChangePassword.js | 5 +++-- .../views/settings/EnableNotificationsButton.js | 10 +++++----- src/components/views/settings/Notifications.js | 3 ++- src/components/views/voip/CallPreview.js | 3 ++- src/components/views/voip/CallView.js | 3 ++- src/components/views/voip/IncomingCallBox.js | 3 ++- src/components/views/voip/VideoFeed.js | 5 ++--- src/components/views/voip/VideoView.js | 5 ++--- 35 files changed, 90 insertions(+), 85 deletions(-) diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index afc6faa18d..f6afee0eba 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -18,12 +18,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'BaseAvatar', propTypes: { diff --git a/src/components/views/avatars/GroupAvatar.js b/src/components/views/avatars/GroupAvatar.js index 5a18213eec..e8ef2a5279 100644 --- a/src/components/views/avatars/GroupAvatar.js +++ b/src/components/views/avatars/GroupAvatar.js @@ -16,10 +16,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; -export default React.createClass({ +export default createReactClass({ displayName: 'GroupAvatar', propTypes: { diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index a9db1165e8..383bab5e79 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -14,15 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; const Avatar = require('../../../Avatar'); const sdk = require("../../../index"); const dispatcher = require("../../../dispatcher"); -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MemberAvatar', propTypes: { diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index 557a4d8dbf..8da5523d1b 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from "react"; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import {ContentRepo} from "matrix-js-sdk"; import MatrixClientPeg from "../../../MatrixClientPeg"; import Modal from '../../../Modal'; import sdk from "../../../index"; import Avatar from '../../../Avatar'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomAvatar', // Room may be left unset here, but if it is, diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 04bc7c75ef..d6d211a985 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import {EventStatus} from 'matrix-js-sdk'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -33,7 +34,7 @@ function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MessageContextMenu', propTypes: { diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index f3a36b6ced..eba8254c03 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -18,8 +18,9 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; -import classNames from 'classnames'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; +import classNames from 'classnames'; import sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -31,7 +32,7 @@ import Modal from '../../../Modal'; import RoomListActions from '../../../actions/RoomListActions'; import RoomViewStore from '../../../stores/RoomViewStore'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomTileContextMenu', propTypes: { diff --git a/src/components/views/create_room/CreateRoomButton.js b/src/components/views/create_room/CreateRoomButton.js index 25f71f542d..1c44aed78c 100644 --- a/src/components/views/create_room/CreateRoomButton.js +++ b/src/components/views/create_room/CreateRoomButton.js @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ + +module.exports = createReactClass({ displayName: 'CreateRoomButton', propTypes: { onCreateRoom: PropTypes.func, diff --git a/src/components/views/create_room/Presets.js b/src/components/views/create_room/Presets.js index c9607c0082..f512c3e2fd 100644 --- a/src/components/views/create_room/Presets.js +++ b/src/components/views/create_room/Presets.js @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from "react"; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; const Presets = { @@ -26,7 +25,7 @@ const Presets = { Custom: "custom", }; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'CreateRoomPresets', propTypes: { onChange: PropTypes.func, diff --git a/src/components/views/create_room/RoomAlias.js b/src/components/views/create_room/RoomAlias.js index 6262db7833..fd3e3365f7 100644 --- a/src/components/views/create_room/RoomAlias.js +++ b/src/components/views/create_room/RoomAlias.js @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomAlias', propTypes: { // Specifying a homeserver will make magical things happen when you, diff --git a/src/components/views/globals/MatrixToolbar.js b/src/components/views/globals/MatrixToolbar.js index efcbfcba48..aabf0810f8 100644 --- a/src/components/views/globals/MatrixToolbar.js +++ b/src/components/views/globals/MatrixToolbar.js @@ -14,14 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import Notifier from '../../../Notifier'; import AccessibleButton from '../../../components/views/elements/AccessibleButton'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MatrixToolbar', hideToolbar: function() { diff --git a/src/components/views/globals/NewVersionBar.js b/src/components/views/globals/NewVersionBar.js index 9dfe754ea0..abb9334242 100644 --- a/src/components/views/globals/NewVersionBar.js +++ b/src/components/views/globals/NewVersionBar.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import Modal from '../../../Modal'; import PlatformPeg from '../../../PlatformPeg'; @@ -31,7 +32,7 @@ function checkVersion(ver) { return parts.length == 5 && parts[1] == 'react' && parts[3] == 'js'; } -export default React.createClass({ +export default createReactClass({ propTypes: { version: PropTypes.string.isRequired, newVersion: PropTypes.string.isRequired, diff --git a/src/components/views/globals/PasswordNagBar.js b/src/components/views/globals/PasswordNagBar.js index 71901ad922..0a4996d0ce 100644 --- a/src/components/views/globals/PasswordNagBar.js +++ b/src/components/views/globals/PasswordNagBar.js @@ -16,11 +16,12 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ onUpdateClicked: function() { const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog'); Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog); diff --git a/src/components/views/globals/ServerLimitBar.js b/src/components/views/globals/ServerLimitBar.js index 0b924fd2e2..7d414a2826 100644 --- a/src/components/views/globals/ServerLimitBar.js +++ b/src/components/views/globals/ServerLimitBar.js @@ -16,11 +16,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import classNames from 'classnames'; import { _td } from '../../../languageHandler'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -export default React.createClass({ +export default createReactClass({ propTypes: { // 'hard' if the logged in user has been locked out, 'soft' if they haven't kind: PropTypes.string, diff --git a/src/components/views/globals/UpdateCheckBar.js b/src/components/views/globals/UpdateCheckBar.js index a215c455eb..32b38ff5b0 100644 --- a/src/components/views/globals/UpdateCheckBar.js +++ b/src/components/views/globals/UpdateCheckBar.js @@ -16,11 +16,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import PlatformPeg from '../../../PlatformPeg'; import AccessibleButton from '../../../components/views/elements/AccessibleButton'; -export default React.createClass({ +export default createReactClass({ propTypes: { status: PropTypes.string.isRequired, // Currently for error detail but will be usable for download progress diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index d7452632e1..7f4d76747a 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -15,10 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import filesize from 'filesize'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; @@ -195,7 +194,7 @@ function computedStyle(element) { return cssText; } -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MFileBody', getInitialState: function() { diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index e864b983d3..d277b6eae9 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import MFileBody from './MFileBody'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { decryptFile } from '../../../utils/DecryptFile'; @@ -25,7 +24,7 @@ import Promise from 'bluebird'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MVideoBody', propTypes: { diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 6d7aada542..a616dd96ed 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -14,13 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; -const sdk = require('../../../index'); +import createReactClass from 'create-react-class'; +import sdk from '../../../index'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'MessageEvent', propTypes: { diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index 207a385b92..513e104d12 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -17,13 +17,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomAvatarEvent', propTypes: { diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js index 95254323fa..bf0ef32460 100644 --- a/src/components/views/messages/RoomCreate.js +++ b/src/components/views/messages/RoomCreate.js @@ -16,13 +16,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import dis from '../../../dispatcher'; import { RoomPermalinkCreator } from '../../../matrix-to'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'RoomCreate', propTypes: { diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 637a56727f..89f4c61b27 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -14,17 +14,16 @@ limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import {MatrixClient} from 'matrix-js-sdk'; import Flair from '../elements/Flair.js'; import FlairStore from '../../../stores/FlairStore'; import { _t } from '../../../languageHandler'; import {getUserNameColorClass} from '../../../utils/FormattingUtils'; -export default React.createClass({ +export default createReactClass({ displayName: 'SenderProfile', propTypes: { mxEvent: PropTypes.object.isRequired, // event whose sender we're showing diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 95b733c5f3..7143d02e74 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -16,17 +16,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import highlight from 'highlight.js'; import * as HtmlUtils from '../../../HtmlUtils'; import {formatDate} from '../../../DateUtils'; import sdk from '../../../index'; import Modal from '../../../Modal'; -import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; import * as ContextualMenu from '../../structures/ContextualMenu'; @@ -36,7 +34,7 @@ import {host as matrixtoHost} from '../../../matrix-to'; import {pillifyLinks} from '../../../utils/pillify'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'TextualBody', propTypes: { diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.js index a46e6cf8ec..be9adeed77 100644 --- a/src/components/views/messages/TextualEvent.js +++ b/src/components/views/messages/TextualEvent.js @@ -14,14 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; const TextForEvent = require('../../../TextForEvent'); -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'TextualEvent', propTypes: { diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 9a172baf7c..ed2306de4f 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -14,12 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'UnknownBody', render: function() { diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index ab25b3bf66..aab6c04f53 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -13,15 +13,13 @@ 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 Promise from 'bluebird'; -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; -const sdk = require('../../../index'); -const Tinter = require('../../../Tinter'); -const MatrixClientPeg = require("../../../MatrixClientPeg"); -const Modal = require("../../../Modal"); - +import Tinter from '../../../Tinter'; import dis from '../../../dispatcher'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; @@ -43,7 +41,7 @@ const ROOM_COLORS = [ // has a high possibility of being used in the nearish future. // Ref: https://github.com/vector-im/riot-web/issues/8421 -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ColorSettings', propTypes: { diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js index 1662692164..7a8332cc9f 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.js +++ b/src/components/views/room_settings/UrlPreviewSettings.js @@ -16,16 +16,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; -const sdk = require("../../../index"); +import createReactClass from 'create-react-class'; +import sdk from "../../../index"; import { _t, _td } from '../../../languageHandler'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import dis from "../../../dispatcher"; import MatrixClientPeg from "../../../MatrixClientPeg"; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'UrlPreviewSettings', propTypes: { diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 7164aa4bbe..32521006c7 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; -const MatrixClientPeg = require("../../../MatrixClientPeg"); -const sdk = require('../../../index'); +import createReactClass from 'create-react-class'; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ChangeAvatar', propTypes: { initialAvatarUrl: PropTypes.string, diff --git a/src/components/views/settings/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.js index afe1521f0f..90c761ba3d 100644 --- a/src/components/views/settings/ChangeDisplayName.js +++ b/src/components/views/settings/ChangeDisplayName.js @@ -16,11 +16,12 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ChangeDisplayName', _getDisplayName: async function() { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index ba708beaf4..a086efaa6d 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -17,8 +17,9 @@ limitations under the License. import Field from "../elements/Field"; -const React = require('react'); +import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; const MatrixClientPeg = require("../../../MatrixClientPeg"); const Modal = require("../../../Modal"); const sdk = require("../../../index"); @@ -30,7 +31,7 @@ import { _t } from '../../../languageHandler'; import sessionStore from '../../../stores/SessionStore'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'ChangePassword', propTypes: { diff --git a/src/components/views/settings/EnableNotificationsButton.js b/src/components/views/settings/EnableNotificationsButton.js index c2f801b60a..1f65c39e6e 100644 --- a/src/components/views/settings/EnableNotificationsButton.js +++ b/src/components/views/settings/EnableNotificationsButton.js @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; -const React = require("react"); -const Notifier = require("../../../Notifier"); -const dis = require("../../../dispatcher"); +import React from "react"; +import createReactClass from 'create-react-class'; +import Notifier from "../../../Notifier"; +import dis from "../../../dispatcher"; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'EnableNotificationsButton', componentDidMount: function() { diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 9b5688aa6a..e3b4cfe122 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import Promise from 'bluebird'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -62,7 +63,7 @@ function portLegacyActions(actions) { } } -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'Notifications', phases: { diff --git a/src/components/views/voip/CallPreview.js b/src/components/views/voip/CallPreview.js index 5c0a1b4370..15c30dcb5b 100644 --- a/src/components/views/voip/CallPreview.js +++ b/src/components/views/voip/CallPreview.js @@ -16,12 +16,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import RoomViewStore from '../../../stores/RoomViewStore'; import CallHandler from '../../../CallHandler'; import dis from '../../../dispatcher'; import sdk from '../../../index'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'CallPreview', propTypes: { diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index 1a84d23f9b..a4d7927ac3 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import dis from '../../../dispatcher'; import CallHandler from '../../../CallHandler'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'CallView', propTypes: { diff --git a/src/components/views/voip/IncomingCallBox.js b/src/components/views/voip/IncomingCallBox.js index 43c339d182..2a2839d103 100644 --- a/src/components/views/voip/IncomingCallBox.js +++ b/src/components/views/voip/IncomingCallBox.js @@ -16,12 +16,13 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import MatrixClientPeg from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'IncomingCallBox', propTypes: { diff --git a/src/components/views/voip/VideoFeed.js b/src/components/views/voip/VideoFeed.js index 23dc236d62..6043c3675a 100644 --- a/src/components/views/voip/VideoFeed.js +++ b/src/components/views/voip/VideoFeed.js @@ -14,12 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'VideoFeed', propTypes: { diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js index d9843042ef..4cc1ef0805 100644 --- a/src/components/views/voip/VideoView.js +++ b/src/components/views/voip/VideoView.js @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import classNames from 'classnames'; import sdk from '../../../index'; @@ -35,7 +34,7 @@ function getFullScreenElement() { ); } -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'VideoView', propTypes: { From 034c35b07e34668de22618bf7101e283455f24ae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Sep 2019 19:19:06 +0100 Subject: [PATCH 367/413] Switch to React 16.9 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 6 ++-- yarn.lock | 86 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index b4e1af9f0a..70de250830 100644 --- a/package.json +++ b/package.json @@ -93,10 +93,10 @@ "qrcode-react": "^0.1.16", "qs": "^6.6.0", "querystring": "^0.2.0", - "react": "^15.6.0", - "react-addons-css-transition-group": "15.3.2", + "react": "^16.9.0", + "react-addons-css-transition-group": "15.6.2", "react-beautiful-dnd": "^4.0.1", - "react-dom": "^15.6.0", + "react-dom": "^16.9.0", "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#f644523", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", diff --git a/yarn.lock b/yarn.lock index 1989f4339a..9cfc36615a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1857,6 +1857,11 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.4.tgz#9cf2de494ca84060a2a8d2854edd6dfb0445f386" integrity sha512-fpZ81yYfzentuieinmGnphk0pLkOTMm6MZdVqwd77ROvhko6iujLNGrHH5E7utq3ygWklwfmwuG+A7P+NpqT6w== +chain-function@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.1.tgz#c63045e5b4b663fb86f1c6e186adaf1de402a1cc" + integrity sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg== + chalk@2.4.2, "chalk@^1.1.3 || 2.x", chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2562,6 +2567,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-helpers@^3.2.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -6165,7 +6177,7 @@ promise@^7.0.3, promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6344,10 +6356,12 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-addons-css-transition-group@15.3.2: - version "15.3.2" - resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.3.2.tgz#d8fa52bec9bb61bdfde8b9e4652b80297cbff667" - integrity sha1-2PpSvsm7Yb396LnkZSuAKXy/9mc= +react-addons-css-transition-group@15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.6.2.tgz#9e4376bcf40b5217d14ec68553081cee4b08a6d6" + integrity sha1-nkN2vPQLUhfRTsaFUwgc7ksIptY= + dependencies: + react-transition-group "^1.2.0" react-beautiful-dnd@^4.0.1: version "4.0.1" @@ -6365,16 +6379,6 @@ react-beautiful-dnd@^4.0.1: redux-thunk "^2.2.0" reselect "^3.0.1" -react-dom@^15.6.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" - integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA= - dependencies: - fbjs "^0.8.9" - loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.10" - react-dom@^16.4.2: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" @@ -6385,6 +6389,16 @@ react-dom@^16.4.2: prop-types "^15.6.2" scheduler "^0.13.6" +react-dom@^16.9.0: + version "16.9.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0.tgz#5e65527a5e26f22ae3701131bcccaee9fb0d3962" + integrity sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.15.0" + "react-gemini-scrollbar@github:matrix-org/react-gemini-scrollbar#f644523": version "2.1.5" resolved "https://codeload.github.com/matrix-org/react-gemini-scrollbar/tar.gz/f64452388011d37d8a4427ba769153c30700ab8c" @@ -6428,16 +6442,16 @@ react-redux@^5.0.6: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" -react@^15.6.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" - integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI= +react-transition-group@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6" + integrity sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q== dependencies: - create-react-class "^15.6.0" - fbjs "^0.8.9" - loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.10" + chain-function "^1.0.0" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.6" + warning "^3.0.0" react@^16.4.2: version "16.8.6" @@ -6449,6 +6463,15 @@ react@^16.4.2: prop-types "^15.6.2" scheduler "^0.13.6" +react@^16.9.0: + version "16.9.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa" + integrity sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -6901,6 +6924,14 @@ scheduler@^0.13.6: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.15.0.tgz#6bfcf80ff850b280fed4aeecc6513bc0b4f17f8e" + integrity sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -8134,6 +8165,13 @@ walk@^2.3.9: dependencies: foreachasync "^3.0.0" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w= + dependencies: + loose-envify "^1.0.0" + watchpack@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" From e29184ae1d43934e36f5b067e520b624a0e39cb4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 6 Sep 2019 13:02:18 -0600 Subject: [PATCH 368/413] Support secret per-room hidden read receipts --- src/components/structures/TimelinePanel.js | 3 ++- src/settings/Settings.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 44569569b6..0ca1cb9996 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -685,7 +685,8 @@ const TimelinePanel = createReactClass({ } this.lastRMSentEventId = this.state.readMarkerEventId; - const hiddenRR = !SettingsStore.getValue("sendReadReceipts"); + const roomId = this.props.timelineSet.room.roomId; + const hiddenRR = !SettingsStore.getValue("sendReadReceipts", roomId); debuglog('TimelinePanel: Sending Read Markers for ', this.props.timelineSet.room.roomId, diff --git a/src/settings/Settings.js b/src/settings/Settings.js index f86a8566c6..7b049208aa 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -395,7 +395,7 @@ export const SETTINGS = { default: null, }, "sendReadReceipts": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, + supportedLevels: LEVELS_ROOM_SETTINGS, displayName: _td( "Send read receipts for messages (requires compatible homeserver to disable)", ), From ef2ff31a46b4f016772072c02802d7bad70683ba Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Sep 2019 09:34:08 +0100 Subject: [PATCH 369/413] Fix replying from search results for this and all rooms Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 6 +++++- src/components/views/rooms/ReplyPreview.js | 9 ++++++--- src/stores/RoomViewStore.js | 21 ++++++++++++++++++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 543b35eaf8..195e4130a6 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -586,7 +586,6 @@ module.exports = createReactClass({ return ContentMessages.sharedInstance().sendContentListToRoom( [payload.file], this.state.room.roomId, MatrixClientPeg.get(), ); - break; case 'notifier_enabled': case 'upload_started': case 'upload_finished': @@ -624,6 +623,11 @@ module.exports = createReactClass({ showApps: payload.show, }); break; + case 'reply_to_event': + if (this.state.searchResults && payload.event.getRoomId() === this.state.roomId && !this.unmounted) { + this.onCancelSearchClick(); + } + break; } }, diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index 3b7874a875..58e7237801 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -37,18 +37,19 @@ export default class ReplyPreview extends React.Component { constructor(props, context) { super(props, context); + this.unmounted = false; this.state = { - event: null, + event: RoomViewStore.getQuotingEvent(), }; this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); - this._onRoomViewStoreUpdate(); } componentWillUnmount() { + this.unmounted = true; + // Remove RoomStore listener if (this._roomStoreToken) { this._roomStoreToken.remove(); @@ -56,6 +57,8 @@ export default class ReplyPreview extends React.Component { } _onRoomViewStoreUpdate() { + if (this.unmounted) return; + const event = RoomViewStore.getQuotingEvent(); if (this.state.event !== event) { this.setState({ event }); diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 166833325e..7e1b06c0bf 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -113,9 +113,19 @@ class RoomViewStore extends Store { }); break; case 'reply_to_event': - this._setState({ - replyingToEvent: payload.event, - }); + // If currently viewed room does not match the room in which we wish to reply then change rooms + // this can happen when performing a search across all rooms + if (payload.event && payload.event.getRoomId() !== this._state.roomId) { + dis.dispatch({ + action: 'view_room', + room_id: payload.event.getRoomId(), + replyingToEvent: payload.event, + }); + } else { + this._setState({ + replyingToEvent: payload.event, + }); + } break; case 'open_room_settings': { const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog"); @@ -147,6 +157,11 @@ class RoomViewStore extends Store { isEditingSettings: false, }; + // Allow being given an event to be replied to when switching rooms but sanity check its for this room + if (payload.replyingToEvent && payload.replyingToEvent.getRoomId() === payload.room_id) { + newState.replyingToEvent = payload.replyingToEvent; + } + if (this._state.forwardingEvent) { dis.dispatch({ action: 'send_event', From c3adddb5ac41ec1516f275a8c1b33eb676fafe1e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 9 Sep 2019 10:27:02 +0100 Subject: [PATCH 370/413] Change to paragraphs outside the strings --- src/components/views/settings/SetIdServer.js | 25 +++++++++++--------- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index afb2d62e0e..af7416518d 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -262,17 +262,20 @@ export default class SetIdServer extends React.Component { let message; let danger = false; if (boundThreepids.length) { - message = _t( - "You are still sharing your personal data on the identity " + - "server .

" + - "We recommend that you remove your email addresses and phone numbers " + - "from the identity server before disconnecting.", {}, - { - idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - b: sub => {sub}, - br: () =>
, - }, - ); + message =
+

{_t( + "You are still sharing your personal data on the identity " + + "server .", {}, + { + idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + b: sub => {sub}, + }, + )}

+

{_t( + "We recommend that you remove your email addresses and phone numbers " + + "from the identity server before disconnecting.", + )}

+
; danger = true; button = _t("Disconnect anyway"); } else { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 02fd05b067..26db20fe93 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -563,7 +563,8 @@ "Disconnect identity server": "Disconnect identity server", "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect": "Disconnect", - "You are still sharing your personal data on the identity server .

We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "You are still sharing your personal data on the identity server .

We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", + "You are still sharing your personal data on the identity server .": "You are still sharing your personal data on the identity server .", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", "Disconnect anyway": "Disconnect anyway", "Go back": "Go back", "Identity Server (%(server)s)": "Identity Server (%(server)s)", From 088f9e4cc5670cf93a17d6a9f0a2f6ff9c6e4964 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Sep 2019 12:50:05 +0100 Subject: [PATCH 371/413] Catch error from changing room power level requirements and show modal Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/tabs/room/RolesRoomSettingsTab.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js index 002748694c..64f3992ad0 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js @@ -148,7 +148,18 @@ export default class RolesRoomSettingsTab extends React.Component { parentObj[keyPath[keyPath.length - 1]] = value; } - client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent); + client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Power level requirement change failed', '', ErrorDialog, { + title: _t('Error changing power level requirement'), + description: _t( + "An error occurred changing the room's power level requirements. Ensure you have sufficient " + + "permissions and try again.", + ), + }); + }); }; _onUserPowerLevelChanged = (value, powerLevelKey) => { From 9c488426cc6a20fd905cdabe3acf2d1ab30fdbb2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Sep 2019 12:51:30 +0100 Subject: [PATCH 372/413] add i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6529e7322c..5c7a60f58b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -690,6 +690,8 @@ "Failed to unban": "Failed to unban", "Unban": "Unban", "Banned by %(displayName)s": "Banned by %(displayName)s", + "Error changing power level requirement": "Error changing power level requirement", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.", "Error changing power level": "Error changing power level", "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.", "Default role": "Default role", From f205ddbc8f2c249154a5c0672e58342c18cc9211 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Sep 2019 15:10:50 +0200 Subject: [PATCH 373/413] add redact recent messages button in member info --- src/components/views/rooms/MemberInfo.js | 86 +++++++++++++++++++++++- src/i18n/strings/en_EN.json | 7 ++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 26f731d308..f82f655349 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -48,6 +48,8 @@ import SettingsStore from "../../../settings/SettingsStore"; import E2EIcon from "./E2EIcon"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import MatrixClientPeg from "../../../MatrixClientPeg"; +import Matrix from "matrix-js-sdk"; +const EventTimeline = Matrix.EventTimeline; module.exports = createReactClass({ displayName: 'MemberInfo', @@ -64,6 +66,7 @@ module.exports = createReactClass({ mute: false, modifyLevel: false, synapseDeactivate: false, + redactMessages: false, }, muted: false, isTargetMod: false, @@ -356,6 +359,73 @@ module.exports = createReactClass({ }); }, + onRedactAllMessages: async function() { + const {roomId, userId} = this.props.member; + const room = this.context.matrixClient.getRoom(roomId); + if (!room) { + return; + } + let timeline = room.getLiveTimeline(); + let eventsToRedact = []; + while (timeline) { + eventsToRedact = timeline.getEvents().reduce((events, event) => { + if (event.getSender() === userId && !event.isRedacted()) { + return events.concat(event); + } else { + return events; + } + }, eventsToRedact); + timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); + } + + const count = eventsToRedact.length; + const user = this.props.member.name; + + if (count === 0) { + const InfoDialog = sdk.getComponent("dialogs.InfoDialog"); + Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, { + title: _t("No recent messages by %(user)s found", {user}), + description: +
+

{ _t("Try scrolling up in the timeline to see if there are any earlier ones.") }

+
, + }); + } else { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const confirmed = await new Promise((resolve) => { + Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, { + title: _t("Remove recent messages by %(user)s", {user}), + description: +
+

{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }

+

{ _t("For large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

+
, + button: _t("Remove %(count)s messages", {count}), + onFinished: resolve, + }); + }); + + if (!confirmed) { + return; + } + + // Submitting a large number of redactions freezes the UI, + // so first wait 200ms to allow to rerender after closing the dialog. + await new Promise(resolve => setTimeout(resolve, 200)); + + await Promise.all(eventsToRedact.map(async event => { + try { + await this.context.matrixClient.redactEvent(roomId, event.getId()); + } catch (err) { + // log and swallow errors + console.error("Could not redact", event.getId()); + console.error(err); + } + })); + console.log("Done redacting recent messages!"); + } + }, + _warnSelfDemote: function() { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); return new Promise((resolve) => { @@ -602,6 +672,7 @@ module.exports = createReactClass({ mute: false, modifyLevel: false, modifyLevelMax: 0, + redactMessages: false, }; // Calculate permissions for Synapse before doing the PL checks @@ -623,6 +694,7 @@ module.exports = createReactClass({ can.mute = me.powerLevel >= editPowerLevel; can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel); can.modifyLevelMax = me.powerLevel; + can.redactMessages = me.powerLevel >= powerLevels.redact; return can; }, @@ -812,6 +884,7 @@ module.exports = createReactClass({ let banButton; let muteButton; let giveModButton; + let redactButton; let synapseDeactivateButton; let spinner; @@ -892,6 +965,16 @@ module.exports = createReactClass({ ); } + + + if (this.state.can.redactMessages) { + redactButton = ( + + { _t("Remove recent messages") } + + ); + } + if (this.state.can.ban) { let label = _t("Ban"); if (this.props.member.membership === 'ban') { @@ -932,7 +1015,7 @@ module.exports = createReactClass({ } let adminTools; - if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton) { + if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton || redactButton) { adminTools =

{ _t("Admin Tools") }

@@ -941,6 +1024,7 @@ module.exports = createReactClass({ { muteButton } { kickButton } { banButton } + { redactButton } { giveModButton } { synapseDeactivateButton }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ed7b180312..fac4d53061 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -794,6 +794,12 @@ "Unban this user?": "Unban this user?", "Ban this user?": "Ban this user?", "Failed to ban user": "Failed to ban user", + "No recent messages by %(user)s found": "No recent messages by %(user)s found", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", + "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", + "For large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For large amount of messages, this might take some time. Please don't refresh your client in the meantime.", + "Remove %(count)s messages|other": "Remove %(count)s messages", "Demote yourself?": "Demote yourself?", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", "Demote": "Demote", @@ -813,6 +819,7 @@ "Share Link to User": "Share Link to User", "User Options": "User Options", "Direct chats": "Direct chats", + "Remove recent messages": "Remove recent messages", "Unmute": "Unmute", "Mute": "Mute", "Revoke Moderator": "Revoke Moderator", From 3edf345b025125ce211964eed63d652cea4cffb3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Sep 2019 16:19:10 +0200 Subject: [PATCH 374/413] PR feedback --- src/components/views/rooms/MemberInfo.js | 13 ++++++------- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index f82f655349..cb11cf39ad 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -48,8 +48,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import E2EIcon from "./E2EIcon"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import MatrixClientPeg from "../../../MatrixClientPeg"; -import Matrix from "matrix-js-sdk"; -const EventTimeline = Matrix.EventTimeline; +import {EventTimeline} from "matrix-js-sdk"; module.exports = createReactClass({ displayName: 'MemberInfo', @@ -398,7 +397,7 @@ module.exports = createReactClass({ description:

{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }

-

{ _t("For large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

+

{ _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

, button: _t("Remove %(count)s messages", {count}), onFinished: resolve, @@ -410,9 +409,10 @@ module.exports = createReactClass({ } // Submitting a large number of redactions freezes the UI, - // so first wait 200ms to allow to rerender after closing the dialog. - await new Promise(resolve => setTimeout(resolve, 200)); + // so first yield to allow to rerender after closing the dialog. + await Promise.resolve(); + console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`); await Promise.all(eventsToRedact.map(async event => { try { await this.context.matrixClient.redactEvent(roomId, event.getId()); @@ -422,7 +422,7 @@ module.exports = createReactClass({ console.error(err); } })); - console.log("Done redacting recent messages!"); + console.info(`Finished redacting recent ${count} messages for ${user} in ${roomId}`); } }, @@ -966,7 +966,6 @@ module.exports = createReactClass({ ); } - if (this.state.can.redactMessages) { redactButton = ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fac4d53061..3f0787523c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -798,7 +798,7 @@ "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", - "For large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For large amount of messages, this might take some time. Please don't refresh your client in the meantime.", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", "Remove %(count)s messages|other": "Remove %(count)s messages", "Demote yourself?": "Demote yourself?", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", From 0b56e7a81c1f54b675a02d6b3f3bab637d90d270 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Sep 2019 08:26:10 +0100 Subject: [PATCH 375/413] Support Synapse deactivate on MemberInfo without Room (timeline pill) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MemberInfo.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index cb11cf39ad..dc9f1070fd 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -642,7 +642,10 @@ module.exports = createReactClass({ _calculateOpsPermissions: async function(member) { const defaultPerms = { - can: {}, + can: { + // Calculate permissions for Synapse before doing the PL checks + synapseDeactivate: await this.context.matrixClient.isSynapseAdministrator(), + }, muted: false, }; const room = this.context.matrixClient.getRoom(member.roomId); @@ -656,9 +659,10 @@ module.exports = createReactClass({ const them = member; return { - can: await this._calculateCanPermissions( - me, them, powerLevels.getContent(), - ), + can: { + ...defaultPerms.can, + ...await this._calculateCanPermissions(me, them, powerLevels.getContent()), + }, muted: this._isMuted(them, powerLevels.getContent()), isTargetMod: them.powerLevel > powerLevels.getContent().users_default, }; @@ -675,9 +679,6 @@ module.exports = createReactClass({ redactMessages: false, }; - // Calculate permissions for Synapse before doing the PL checks - can.synapseDeactivate = await this.context.matrixClient.isSynapseAdministrator(); - const canAffectUser = them.powerLevel < me.powerLevel || isMe; if (!canAffectUser) { //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); From 80add7be927c8d59eced97ffc4e88ebbb941df8e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Sep 2019 08:38:51 +0100 Subject: [PATCH 376/413] delint more properly Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 195e4130a6..3446901331 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -583,9 +583,10 @@ module.exports = createReactClass({ payload.data.description || payload.data.name); break; case 'picture_snapshot': - return ContentMessages.sharedInstance().sendContentListToRoom( + ContentMessages.sharedInstance().sendContentListToRoom( [payload.file], this.state.room.roomId, MatrixClientPeg.get(), ); + break; case 'notifier_enabled': case 'upload_started': case 'upload_finished': From 80dd5a1b0a576bcf4f3d7c242e3da9c9e94ba52f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 10:53:55 +0200 Subject: [PATCH 377/413] add explore button next to filter field --- res/css/structures/_LeftPanel.scss | 41 +++++++++++ res/css/structures/_SearchBox.scss | 21 +++--- res/css/views/rooms/_RoomList.scss | 4 -- res/img/explore.svg | 97 ++++++++++++++++++++++++++ src/components/structures/LeftPanel.js | 15 +++- src/i18n/strings/en_EN.json | 1 + 6 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 res/img/explore.svg diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 7d10fdb6d6..e01dfb75cd 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -125,3 +125,44 @@ limitations under the License. margin-top: 12px; } } + +.mx_LeftPanel_exploreAndFilterRow { + display: flex; + + .mx_SearchBox { + flex: 1 1 0; + min-width: 0; + } +} + +.mx_LeftPanel_explore { + flex: 0 0 40%; + overflow: hidden; + box-sizing: border-box; + + .mx_AccessibleButton { + font-size: 14px; + margin: 9px; + margin-right: 0; + padding: 9px; + padding-left: 42px; + font-weight: 600; + color: $notice-secondary-color; + position: relative; + border-radius: 4px; + + &::before { + cursor: pointer; + mask: url('$(res)/img/explore.svg'); + mask-repeat: no-repeat; + mask-position: center center; + content: ""; + left: 14px; + top: 10px; + width: 16px; + height: 16px; + background-color: $notice-secondary-color; + position: absolute; + } + } +} diff --git a/res/css/structures/_SearchBox.scss b/res/css/structures/_SearchBox.scss index 9434d93bd2..7d13405478 100644 --- a/res/css/structures/_SearchBox.scss +++ b/res/css/structures/_SearchBox.scss @@ -14,12 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SearchBox_closeButton { - cursor: pointer; - background-image: url('$(res)/img/icons-close.svg'); - background-repeat: no-repeat; - width: 16px; - height: 16px; - background-position: center; - padding: 9px; +.mx_SearchBox { + flex: 1 1 0; + min-width: 0; + + .mx_SearchBox_closeButton { + cursor: pointer; + background-image: url('$(res)/img/icons-close.svg'); + background-repeat: no-repeat; + width: 16px; + height: 16px; + background-position: center; + padding: 9px; + } } diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index b51d720e4d..5ed22f997d 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -27,10 +27,6 @@ limitations under the License. position: relative; } -.mx_SearchBox { - flex: none; -} - /* hide resize handles next to collapsed / empty sublists */ .mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle { display: none; diff --git a/res/img/explore.svg b/res/img/explore.svg new file mode 100644 index 0000000000..3956e912ac --- /dev/null +++ b/res/img/explore.svg @@ -0,0 +1,97 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index f083e5ab38..ab1190d8b9 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -217,6 +217,7 @@ const LeftPanel = createReactClass({ const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton'); const SearchBox = sdk.getComponent('structures.SearchBox'); const CallPreview = sdk.getComponent('voip.CallPreview'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel"); let tagPanelContainer; @@ -240,6 +241,15 @@ const LeftPanel = createReactClass({ }, ); + let exploreButton; + if (!this.props.collapsed) { + exploreButton = ( +
+ dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")} +
+ ); + } + const searchBox = ( { breadcrumbs } - { searchBox } +
+ { exploreButton } + { searchBox } +
Date: Tue, 10 Sep 2019 10:57:25 +0200 Subject: [PATCH 378/413] hide explore button when focusing filter field --- res/css/structures/_LeftPanel.scss | 5 +++++ src/components/structures/LeftPanel.js | 13 +++++++++++++ src/components/structures/SearchBox.js | 10 ++++++++++ 3 files changed, 28 insertions(+) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index e01dfb75cd..c2b8a6873f 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -138,8 +138,13 @@ limitations under the License. .mx_LeftPanel_explore { flex: 0 0 40%; overflow: hidden; + transition: flex-basis 0.2s; box-sizing: border-box; + &.mx_LeftPanel_explore_hidden { + flex-basis: 0; + } + .mx_AccessibleButton { font-size: 14px; margin: 9px; diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index ab1190d8b9..b77b5e2e39 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -81,6 +81,9 @@ const LeftPanel = createReactClass({ if (this.state.searchFilter !== nextState.searchFilter) { return true; } + if (this.state.searchFocused !== nextState.searchFocused) { + return true; + } return false; }, @@ -209,6 +212,14 @@ const LeftPanel = createReactClass({ this._roomList = ref; }, + _onSearchFocus: function() { + this.setState({searchFocused: true}); + }, + + _onSearchBlur: function() { + this.setState({searchFocused: false}); + }, + render: function() { const RoomList = sdk.getComponent('rooms.RoomList'); const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs'); @@ -255,6 +266,8 @@ const LeftPanel = createReactClass({ placeholder={ _t('Filter room names') } onSearch={ this.onSearch } onCleared={ this.onSearchCleared } + onFocus={this._onSearchFocus} + onBlur={this._onSearchBlur} collapsed={this.props.collapsed} />); let breadcrumbs; diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index d8ff08adbf..70e898fe04 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -95,6 +95,15 @@ module.exports = createReactClass({ _onFocus: function(ev) { ev.target.select(); + if (this.props.onFocus) { + this.props.onFocus(ev); + } + }, + + _onBlur: function(ev) { + if (this.props.onBlur) { + this.props.onBlur(ev); + } }, _clearSearch: function(source) { @@ -132,6 +141,7 @@ module.exports = createReactClass({ onChange={ this.onChange } onKeyDown={ this._onKeyDown } placeholder={ this.props.placeholder } + onBlur={this._onBlur} /> { clearButton }
From 15d37746651d9b9f8162859539d7f8ebcfc14f39 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 10:58:11 +0200 Subject: [PATCH 379/413] show shorter placeholder for filter feed when not focused --- src/components/structures/LeftPanel.js | 3 ++- src/components/structures/SearchBox.js | 12 +++++++++++- src/i18n/strings/en_EN.json | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index b77b5e2e39..4c5b1686f5 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -263,7 +263,8 @@ const LeftPanel = createReactClass({ const searchBox = ( {this._clearSearch("button"); } }> ) : undefined; + // show a shorter placeholder when blurred, if requested + // this is used for the room filter field that has + // the explore button next to it when blurred + const placeholder = this.state.blurred ? + (this.props.blurredPlaceholder || this.props.placeholder) : + this.props.placeholder; const className = this.props.className || ""; return (
@@ -140,8 +150,8 @@ module.exports = createReactClass({ onFocus={ this._onFocus } onChange={ this.onChange } onKeyDown={ this._onKeyDown } - placeholder={ this.props.placeholder } onBlur={this._onBlur} + placeholder={ placeholder } /> { clearButton }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b7220e3082..48a9e25857 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1549,8 +1549,9 @@ "Community %(groupId)s not found": "Community %(groupId)s not found", "This homeserver does not support communities": "This homeserver does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", - "Filter room names": "Filter room names", "Explore": "Explore", + "Filter": "Filter", + "Filter rooms…": "Filter rooms…", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", From 1c4093eb0f3b37701b1a7dd6e726bbfbb989f6ca Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 10:58:44 +0200 Subject: [PATCH 380/413] make filter feed transparent when not focussed --- res/css/structures/_SearchBox.scss | 4 ++++ src/components/structures/SearchBox.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_SearchBox.scss b/res/css/structures/_SearchBox.scss index 7d13405478..23ee06f7b3 100644 --- a/res/css/structures/_SearchBox.scss +++ b/res/css/structures/_SearchBox.scss @@ -18,6 +18,10 @@ limitations under the License. flex: 1 1 0; min-width: 0; + &.mx_SearchBox_blurred:not(:hover) { + background-color: transparent; + } + .mx_SearchBox_closeButton { cursor: pointer; background-image: url('$(res)/img/icons-close.svg'); diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index a381416a80..4d68ff4e96 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -140,7 +140,7 @@ module.exports = createReactClass({ this.props.placeholder; const className = this.props.className || ""; return ( -
+
Date: Tue, 10 Sep 2019 10:59:22 +0200 Subject: [PATCH 381/413] make explore button white on hover --- res/css/structures/_LeftPanel.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index c2b8a6873f..f83195f847 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -156,6 +156,10 @@ limitations under the License. position: relative; border-radius: 4px; + &:hover { + background-color: $primary-bg-color; + } + &::before { cursor: pointer; mask: url('$(res)/img/explore.svg'); From 4148f1697cd6bab2d183030f653d112c80d84572 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 10:59:36 +0200 Subject: [PATCH 382/413] make input fields on a dark panel have a white background (filter field) --- res/css/_common.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index adf4c93290..77b474a013 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -171,7 +171,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search], .mx_textinput { color: $input-darker-fg-color; - background-color: $input-darker-bg-color; + background-color: $primary-bg-color; border: none; } } From 68dde07f49d5be72caf54e38720715e46f750060 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 11:59:59 +0200 Subject: [PATCH 383/413] make add room button go to create room dialog instead of room directory --- src/components/structures/RoomDirectory.js | 11 ----------- src/components/views/rooms/RoomList.js | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index aa2e56d3c2..13ae83b067 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -321,11 +321,6 @@ module.exports = createReactClass({ } }, - onCreateRoomClicked: function() { - this.props.onFinished(); - dis.dispatch({action: 'view_create_room'}); - }, - onJoinClick: function(alias) { // If we don't have a particular instance id selected, just show that rooms alias if (!this.state.instanceId) { @@ -602,17 +597,11 @@ module.exports = createReactClass({
; } - const createRoomButton = ({_t("Create new room")}); - return (
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2454f012f8..da2d11f34b 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -758,7 +758,7 @@ module.exports = createReactClass({ headerItems: this._getHeaderItems('im.vector.fake.recent'), order: "recent", incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), - onAddRoom: () => {dis.dispatch({action: 'view_room_directory'})}, + onAddRoom: () => {dis.dispatch({action: 'view_create_room'});}, }, ]; const tagSubLists = Object.keys(this.state.lists) From 4fbedec013b006da8de517b7d47f5e2a69be67ab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 10 Sep 2019 11:01:20 -0600 Subject: [PATCH 384/413] Make uses of AddressPickerDialog static dialogs Fixes https://github.com/vector-im/riot-web/issues/10603 Static dialogs are ones that stay open underneath other dialogs, like the terms of service prompt. This is how user/room settings operate. --- src/GroupAddressPicker.js | 4 ++-- src/RoomInvite.js | 4 ++-- src/components/structures/GroupView.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index cd5ecc790d..dfc90841a3 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -46,7 +46,7 @@ export function showGroupInviteDialog(groupId) { _onGroupInviteFinished(groupId, addrs).then(resolve, reject); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }); } @@ -81,7 +81,7 @@ export function showGroupAddRoomDialog(groupId) { _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly).then(resolve, reject); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }); } diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 856a2ca577..ec99cd8e9a 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -62,7 +62,7 @@ export function showStartChatInviteDialog() { validAddressTypes, button: _t("Start Chat"), onFinished: _onStartDmFinished, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); } export function showRoomInviteDialog(roomId) { @@ -88,7 +88,7 @@ export function showRoomInviteDialog(roomId) { onFinished: (shouldInvite, addrs) => { _onRoomInviteFinished(roomId, shouldInvite, addrs); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); } /** diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index ed18f0f463..70d8b2e298 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -120,7 +120,7 @@ const CategoryRoomList = createReactClass({ }); }); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, render: function() { @@ -297,7 +297,7 @@ const RoleUserList = createReactClass({ }); }); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, render: function() { From 312143315bba2a8863cc9357858199a175ac62d5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 10 Sep 2019 11:13:35 -0600 Subject: [PATCH 385/413] Only update m.accepted_terms if there were changes Fixes https://github.com/vector-im/riot-web/issues/10744 --- src/Terms.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Terms.js b/src/Terms.js index 594f15b522..a664d3f72e 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -116,6 +116,7 @@ export async function startTermsFlow( } // if there's anything left to agree to, prompt the user + let numAcceptedBeforeAgreement = agreedUrlSet.size; if (unagreedPoliciesAndServicePairs.length > 0) { const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]); console.log("User has agreed to URLs", newlyAgreedUrls); @@ -125,8 +126,11 @@ export async function startTermsFlow( console.log("User has already agreed to all required policies"); } - const newAcceptedTerms = { accepted: Array.from(agreedUrlSet) }; - await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms); + // We only ever add to the set of URLs, so if anything has changed then we'd see a different length + if (agreedUrlSet.size !== numAcceptedBeforeAgreement) { + const newAcceptedTerms = {accepted: Array.from(agreedUrlSet)}; + await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms); + } const agreePromises = policiesAndServicePairs.map((policiesAndService) => { // filter the agreed URL list for ones that are actually for this service From b35d56167e398ecc834df78d26f2ab580520f9f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 10 Sep 2019 11:16:45 -0600 Subject: [PATCH 386/413] const-antly annoying linter although it's a valid complaint here --- src/Terms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terms.js b/src/Terms.js index a664d3f72e..685a39709c 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -116,7 +116,7 @@ export async function startTermsFlow( } // if there's anything left to agree to, prompt the user - let numAcceptedBeforeAgreement = agreedUrlSet.size; + const numAcceptedBeforeAgreement = agreedUrlSet.size; if (unagreedPoliciesAndServicePairs.length > 0) { const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]); console.log("User has agreed to URLs", newlyAgreedUrls); From ad2e16d432454a470cab42827f708153ae3f7299 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 13:46:18 +0200 Subject: [PATCH 387/413] keep filter field expanded if it has text in it --- src/components/structures/LeftPanel.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 4c5b1686f5..fd315d2540 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -81,7 +81,7 @@ const LeftPanel = createReactClass({ if (this.state.searchFilter !== nextState.searchFilter) { return true; } - if (this.state.searchFocused !== nextState.searchFocused) { + if (this.state.searchExpanded !== nextState.searchExpanded) { return true; } @@ -206,6 +206,7 @@ const LeftPanel = createReactClass({ if (source === "keyboard") { dis.dispatch({action: 'focus_composer'}); } + this.setState({searchExpanded: false}); }, collectRoomList: function(ref) { @@ -213,11 +214,13 @@ const LeftPanel = createReactClass({ }, _onSearchFocus: function() { - this.setState({searchFocused: true}); + this.setState({searchExpanded: true}); }, - _onSearchBlur: function() { - this.setState({searchFocused: false}); + _onSearchBlur: function(event) { + if (event.target.value.length === 0) { + this.setState({searchExpanded: false}); + } }, render: function() { @@ -255,7 +258,7 @@ const LeftPanel = createReactClass({ let exploreButton; if (!this.props.collapsed) { exploreButton = ( -
+
dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")}
); From 7754e08d84005a1f7fe4544c5bdd665b304dcd2c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 16:07:01 +0200 Subject: [PATCH 388/413] remove labels and add join/view & preview button instead in directory --- src/components/structures/RoomDirectory.js | 159 ++++++++++++--------- src/i18n/strings/en_EN.json | 2 + 2 files changed, 93 insertions(+), 68 deletions(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 13ae83b067..55078e68be 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -321,7 +321,7 @@ module.exports = createReactClass({ } }, - onJoinClick: function(alias) { + onJoinFromSearchClick: function(alias) { // If we don't have a particular instance id selected, just show that rooms alias if (!this.state.instanceId) { // If the user specified an alias without a domain, add on whichever server is selected @@ -363,6 +363,34 @@ module.exports = createReactClass({ } }, + onPreviewClick: function(room) { + this.props.onFinished(); + dis.dispatch({ + action: 'view_room', + room_id: room.room_id, + should_peek: true, + }); + }, + + onViewClick: function(room) { + this.props.onFinished(); + dis.dispatch({ + action: 'view_room', + room_id: room.room_id, + should_peek: false, + }); + }, + + onJoinClick: function(room) { + this.props.onFinished(); + MatrixClientPeg.get().joinRoom(room.room_id); + dis.dispatch({ + action: 'view_room', + room_id: room.room_id, + joining: true, + }); + }, + showRoomAlias: function(alias, autoJoin=false) { this.showRoom(null, alias, autoJoin); }, @@ -407,74 +435,69 @@ module.exports = createReactClass({ dis.dispatch(payload); }, - getRows: function() { + getRow(room) { + const client = MatrixClientPeg.get(); + const clientRoom = client.getRoom(room.room_id); + const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join"; + const isGuest = client.isGuest(); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let previewButton; + let joinOrViewButton; - if (!this.state.publicRooms) return []; - - const rooms = this.state.publicRooms; - const rows = []; - const self = this; - let guestRead; let guestJoin; let perms; - for (let i = 0; i < rooms.length; i++) { - guestRead = null; - guestJoin = null; - - if (rooms[i].world_readable) { - guestRead = ( -
{ _t('World readable') }
- ); - } - if (rooms[i].guest_can_join) { - guestJoin = ( -
{ _t('Guests can join') }
- ); - } - - perms = null; - if (guestRead || guestJoin) { - perms =
{guestRead}{guestJoin}
; - } - - let name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room'); - if (name.length > MAX_NAME_LENGTH) { - name = `${name.substring(0, MAX_NAME_LENGTH)}...`; - } - - let topic = rooms[i].topic || ''; - if (topic.length > MAX_TOPIC_LENGTH) { - topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; - } - topic = linkifyAndSanitizeHtml(topic); - - rows.push( - {ev.preventDefault();}} - > - - - - -
{ name }
  - { perms } -
-
{ get_display_alias_for_room(rooms[i]) }
- - - { rooms[i].num_joined_members } - - , + if (room.world_readable && !hasJoinedRoom) { + previewButton = ( + this.onPreviewClick(room)}>{_t("Preview")} ); } - return rows; + if (hasJoinedRoom) { + joinOrViewButton = ( + this.onViewClick(room)}>{_t("View")} + ); + } else if (!isGuest || room.guest_can_join) { + joinOrViewButton = ( + this.onJoinClick(room)}>{_t("Join")} + ); + } + + let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room'); + if (name.length > MAX_NAME_LENGTH) { + name = `${name.substring(0, MAX_NAME_LENGTH)}...`; + } + + let topic = room.topic || ''; + if (topic.length > MAX_TOPIC_LENGTH) { + topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; + } + topic = linkifyAndSanitizeHtml(topic); + + return ( + this.onRoomClicked(room)} + // cancel onMouseDown otherwise shift-clicking highlights text + onMouseDown={(ev) => {ev.preventDefault();}} + > + + + + +
{ name }
  +
+
{ get_display_alias_for_room(room) }
+ + + { room.num_joined_members } + + {previewButton} + {joinOrViewButton} + + ); }, collectScrollPanel: function(element) { @@ -528,7 +551,7 @@ module.exports = createReactClass({ } else if (this.state.protocolsLoading || this.state.loading) { content = ; } else { - const rows = this.getRows(); + const rows = (this.state.publicRooms || []).map(room => this.getRow(room)); // we still show the scrollpanel, at least for now, because // otherwise we don't fetch more because we don't get a fill // request from the scrollpanel because there isn't one @@ -538,7 +561,7 @@ module.exports = createReactClass({ } else { scrollpanel_content = - { this.getRows() } + { rows }
; } @@ -590,7 +613,7 @@ module.exports = createReactClass({ listHeader =
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c7ce3cd19b..ebbe055694 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1598,6 +1598,8 @@ "Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room", "Fetching third party location failed": "Fetching third party location failed", "Unable to look up room ID from server": "Unable to look up room ID from server", + "Preview": "Preview", + "View": "View", "Search for a room": "Search for a room", "Search for a room like #example": "Search for a room like #example", "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", From 6f62cdb22cb833aee3cfce32532d8413e3fae08f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 16:07:27 +0200 Subject: [PATCH 389/413] basic styling to make it look ok before applying new design --- res/css/structures/_RoomDirectory.scss | 9 +++++++++ res/css/views/elements/_AccessibleButton.scss | 1 + 2 files changed, 10 insertions(+) diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 1df0a61a2b..6989c3b0b0 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -135,4 +135,13 @@ limitations under the License. .mx_RoomDirectory_table tr { padding-bottom: 10px; cursor: pointer; + + .mx_RoomDirectory_roomDescription { + width: 50%; + } + + .mx_RoomDirectory_join, .mx_RoomDirectory_preview { + width: 80px; + text-align: center; + } } diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 0c081ec0d5..5ca5d002ba 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_AccessibleButton { cursor: pointer; + white-space: nowrap; } .mx_AccessibleButton:focus { From 094c9e346818d4ed1b81cbea9a427555d5b7574a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 14:15:46 +0000 Subject: [PATCH 390/413] consistent naming Co-Authored-By: Travis Ralston --- src/components/structures/RoomDirectory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 55078e68be..d6607461cf 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -487,7 +487,7 @@ module.exports = createReactClass({
{ name }
 
{ ev.stopPropagation(); } } dangerouslySetInnerHTML={{ __html: topic }} />
{ get_display_alias_for_room(room) }
From 77175fcbc4a1d40a3a38bbcc8d1e1c80a8539c79 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 16:18:34 +0200 Subject: [PATCH 391/413] pr feedback --- src/components/structures/RoomDirectory.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index d6607461cf..c4f7ffe82e 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -470,7 +470,10 @@ module.exports = createReactClass({ topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; } topic = linkifyAndSanitizeHtml(topic); - + const avatarUrl = ContentRepo.getHttpUriForMxc( + MatrixClientPeg.get().getHomeserverUrl(), + room.avatar_url, 24, 24, "crop", + ); return ( this.onRoomClicked(room)} @@ -480,15 +483,13 @@ module.exports = createReactClass({ + url={ avatarUrl } />
{ name }
 
{ ev.stopPropagation(); } } - dangerouslySetInnerHTML={{ __html: topic }} /> + onClick={ (ev) => { ev.stopPropagation(); } } + dangerouslySetInnerHTML={{ __html: topic }} />
{ get_display_alias_for_room(room) }
From 24c4a16da177883928e11fea92efe4cf1053f81b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 15:36:29 +0200 Subject: [PATCH 392/413] adapt design of room list in directory --- res/css/structures/_RoomDirectory.scss | 82 +++++++++++----------- src/components/structures/RoomDirectory.js | 26 +++++-- src/i18n/strings/en_EN.json | 6 +- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 6989c3b0b0..6b7a4ff0c7 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -17,7 +17,6 @@ limitations under the License. .mx_RoomDirectory_dialogWrapper > .mx_Dialog { max-width: 960px; height: 100%; - padding: 20px; } .mx_RoomDirectory_dialog { @@ -35,17 +34,6 @@ limitations under the License. flex: 1; } -.mx_RoomDirectory_createRoom { - background-color: $button-bg-color; - border-radius: 4px; - padding: 8px; - color: $button-fg-color; - font-weight: 600; - position: absolute; - top: 0; - left: 0; -} - .mx_RoomDirectory_list { flex: 1; display: flex; @@ -84,9 +72,8 @@ limitations under the License. } .mx_RoomDirectory_roomAvatar { - width: 24px; - padding-left: 12px; - padding-right: 24px; + width: 32px; + padding-right: 14px; vertical-align: top; } @@ -94,6 +81,34 @@ limitations under the License. padding-bottom: 16px; } +.mx_RoomDirectory_roomMemberCount { + color: $light-fg-color; + width: 60px; + padding: 0 10px; + text-align: center; + + &::before { + background-color: $light-fg-color; + display: inline-block; + vertical-align: text-top; + margin-right: 2px; + content: ""; + mask: url('$(res)/img/feather-customised/user.svg'); + mask-repeat: no-repeat; + mask-position: center; + // scale it down and make the size slightly bigger (16 instead of 14px) + // to avoid rendering artifacts + mask-size: 80%; + width: 16px; + height: 16px; + } +} + +.mx_RoomDirectory_join, .mx_RoomDirectory_preview { + width: 80px; + text-align: center; +} + .mx_RoomDirectory_name { display: inline-block; font-weight: 600; @@ -103,22 +118,9 @@ limitations under the License. display: inline-block; } -.mx_RoomDirectory_perm { - display: inline; - padding-left: 5px; - padding-right: 5px; - margin-right: 5px; - height: 15px; - border-radius: 11px; - background-color: $plinth-bg-color; - text-transform: uppercase; - font-weight: 600; - font-size: 11px; - color: $accent-color; -} - .mx_RoomDirectory_topic { cursor: initial; + color: $light-fg-color; } .mx_RoomDirectory_alias { @@ -126,22 +128,20 @@ limitations under the License. color: $settings-grey-fg-color; } -.mx_RoomDirectory_roomMemberCount { - text-align: right; - width: 100px; - padding-right: 10px; -} - .mx_RoomDirectory_table tr { padding-bottom: 10px; cursor: pointer; +} - .mx_RoomDirectory_roomDescription { - width: 50%; - } +.mx_RoomDirectory .mx_RoomView_MessageList { + padding: 0; +} - .mx_RoomDirectory_join, .mx_RoomDirectory_preview { - width: 80px; - text-align: center; +.mx_RoomDirectory p { + font-size: 14px; + margin-top: 0; + + .mx_AccessibleButton { + padding: 0; } } diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index c4f7ffe82e..b85dc20b21 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -391,6 +391,11 @@ module.exports = createReactClass({ }); }, + onCreateRoomClick: function(room) { + this.props.onFinished(); + dis.dispatch({action: 'view_create_room'}); + }, + showRoomAlias: function(alias, autoJoin=false) { this.showRoom(null, alias, autoJoin); }, @@ -472,7 +477,7 @@ module.exports = createReactClass({ topic = linkifyAndSanitizeHtml(topic); const avatarUrl = ContentRepo.getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), - room.avatar_url, 24, 24, "crop", + room.avatar_url, 32, 32, "crop", ); return ( {ev.preventDefault();}} > - @@ -595,10 +600,9 @@ module.exports = createReactClass({ instance_expected_field_type = this.protocols[protocolName].field_types[last_field]; } - - let placeholder = _t('Search for a room'); + let placeholder = _t('Find a room…'); if (!this.state.instanceId) { - placeholder = _t('Search for a room like #example') + ':' + this.state.roomServer; + placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer}); } else if (instance_expected_field_type) { placeholder = instance_expected_field_type.placeholder; } @@ -620,15 +624,25 @@ module.exports = createReactClass({
; } + const explanation = + _t("If you can't find the room you're looking for, ask for an invite or Create a new room.", null, + {a: sub => { + return ({sub}); + }}, + ); return (
+

{explanation}

{listHeader} {content} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ebbe055694..108122d7c9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1600,8 +1600,10 @@ "Unable to look up room ID from server": "Unable to look up room ID from server", "Preview": "Preview", "View": "View", - "Search for a room": "Search for a room", - "Search for a room like #example": "Search for a room like #example", + "Find a room…": "Find a room…", + "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "If you can't find the room you're looking for, ask for an invite or Create a new room.", + "Explore rooms": "Explore rooms", "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", "Show devices, send anyway or cancel.": "Show devices, send anyway or cancel.", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", From 886402fe319a2d0a9003436552accabbf3963404 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 15:36:59 +0200 Subject: [PATCH 393/413] reduce padding on dialogs, as in design --- res/css/_common.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 77b474a013..2b627cce9f 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -330,7 +330,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_header { position: relative; - margin-bottom: 20px; + margin-bottom: 10px; } .mx_Dialog_title { From f04c347df7dad67cc698ca246f12188bfde71803 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Sep 2019 11:33:58 +0100 Subject: [PATCH 394/413] Lift 3PID state management up to Settings tab This pulls the 3PID state management in Settings up to a single location so that the Account and Discovery sections now work from a single list that updates immediately. Fixes https://github.com/vector-im/riot-web/issues/10519 --- src/boundThreepids.js | 5 +-- src/components/views/settings/SetIdServer.js | 4 +-- .../views/settings/account/EmailAddresses.js | 33 ++++++++--------- .../views/settings/account/PhoneNumbers.js | 33 ++++++++--------- .../settings/discovery/EmailAddresses.js | 22 +++--------- .../views/settings/discovery/PhoneNumbers.js | 22 +++--------- .../tabs/user/GeneralUserSettingsTab.js | 35 ++++++++++++++++--- 7 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/boundThreepids.js b/src/boundThreepids.js index 799728f801..90a5bd2a6f 100644 --- a/src/boundThreepids.js +++ b/src/boundThreepids.js @@ -16,7 +16,7 @@ limitations under the License. import IdentityAuthClient from './IdentityAuthClient'; -export async function getThreepidBindStatus(client, filterMedium) { +export async function getThreepidsWithBindStatus(client, filterMedium) { const userId = client.getUserId(); let { threepids } = await client.getThreePids(); @@ -24,7 +24,8 @@ export async function getThreepidBindStatus(client, filterMedium) { threepids = threepids.filter((a) => a.medium === filterMedium); } - if (threepids.length > 0) { + // Check bind status assuming we have an IS and terms are agreed + if (threepids.length > 0 && client.getIdentityServerUrl()) { // TODO: Handle terms agreement // See https://github.com/vector-im/riot-web/issues/10522 const authClient = new IdentityAuthClient(); diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ae550725f1..9ef5fb295e 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -22,7 +22,7 @@ import sdk from '../../../index'; import MatrixClientPeg from "../../../MatrixClientPeg"; import Modal from '../../../Modal'; import dis from "../../../dispatcher"; -import { getThreepidBindStatus } from '../../../boundThreepids'; +import { getThreepidsWithBindStatus } from '../../../boundThreepids'; import IdentityAuthClient from "../../../IdentityAuthClient"; import {SERVICE_TYPES} from "matrix-js-sdk"; import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; @@ -249,7 +249,7 @@ export default class SetIdServer extends React.Component { }; async _showServerChangeWarning({ title, unboundMessage, button }) { - const threepids = await getThreepidBindStatus(MatrixClientPeg.get()); + const threepids = await getThreepidsWithBindStatus(MatrixClientPeg.get()); const boundThreepids = threepids.filter(tp => tp.bound); let message; diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js index eb60d4a322..b7324eb272 100644 --- a/src/components/views/settings/account/EmailAddresses.js +++ b/src/components/views/settings/account/EmailAddresses.js @@ -23,8 +23,8 @@ import Field from "../../elements/Field"; import AccessibleButton from "../../elements/AccessibleButton"; import * as Email from "../../../../email"; import AddThreepid from "../../../../AddThreepid"; -const sdk = require('../../../../index'); -const Modal = require("../../../../Modal"); +import sdk from '../../../../index'; +import Modal from '../../../../Modal'; /* TODO: Improve the UX for everything in here. @@ -113,11 +113,15 @@ export class ExistingEmailAddress extends React.Component { } export default class EmailAddresses extends React.Component { - constructor() { - super(); + static propTypes = { + emails: PropTypes.array.isRequired, + onEmailsChange: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); this.state = { - emails: [], verifying: false, addTask: null, continueDisabled: false, @@ -125,16 +129,9 @@ export default class EmailAddresses extends React.Component { }; } - componentWillMount(): void { - const client = MatrixClientPeg.get(); - - client.getThreePids().then((addresses) => { - this.setState({emails: addresses.threepids.filter((a) => a.medium === 'email')}); - }); - } - _onRemoved = (address) => { - this.setState({emails: this.state.emails.filter((e) => e !== address)}); + const emails = this.props.emails.filter((e) => e !== address); + this.props.onEmailsChange(emails); }; _onChangeNewEmailAddress = (e) => { @@ -184,12 +181,16 @@ export default class EmailAddresses extends React.Component { this.state.addTask.checkEmailLinkClicked().then(() => { const email = this.state.newEmailAddress; this.setState({ - emails: [...this.state.emails, {address: email, medium: "email"}], addTask: null, continueDisabled: false, verifying: false, newEmailAddress: "", }); + const emails = [ + ...this.props.emails, + { address: email, medium: "email" }, + ]; + this.props.onEmailsChange(emails); }).catch((err) => { this.setState({continueDisabled: false}); if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { @@ -204,7 +205,7 @@ export default class EmailAddresses extends React.Component { }; render() { - const existingEmailElements = this.state.emails.map((e) => { + const existingEmailElements = this.props.emails.map((e) => { return ; }); diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js index fbb5b7e561..8f91eb22cc 100644 --- a/src/components/views/settings/account/PhoneNumbers.js +++ b/src/components/views/settings/account/PhoneNumbers.js @@ -23,8 +23,8 @@ import Field from "../../elements/Field"; import AccessibleButton from "../../elements/AccessibleButton"; import AddThreepid from "../../../../AddThreepid"; import CountryDropdown from "../../auth/CountryDropdown"; -const sdk = require('../../../../index'); -const Modal = require("../../../../Modal"); +import sdk from '../../../../index'; +import Modal from '../../../../Modal'; /* TODO: Improve the UX for everything in here. @@ -108,11 +108,15 @@ export class ExistingPhoneNumber extends React.Component { } export default class PhoneNumbers extends React.Component { - constructor() { - super(); + static propTypes = { + msisdns: PropTypes.array.isRequired, + onMsisdnsChange: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); this.state = { - msisdns: [], verifying: false, verifyError: false, verifyMsisdn: "", @@ -124,16 +128,9 @@ export default class PhoneNumbers extends React.Component { }; } - componentWillMount(): void { - const client = MatrixClientPeg.get(); - - client.getThreePids().then((addresses) => { - this.setState({msisdns: addresses.threepids.filter((a) => a.medium === 'msisdn')}); - }); - } - _onRemoved = (address) => { - this.setState({msisdns: this.state.msisdns.filter((e) => e !== address)}); + const msisdns = this.props.msisdns.filter((e) => e !== address); + this.props.onMsisdnsChange(msisdns); }; _onChangeNewPhoneNumber = (e) => { @@ -181,7 +178,6 @@ export default class PhoneNumbers extends React.Component { const token = this.state.newPhoneNumberCode; this.state.addTask.haveMsisdnToken(token).then(() => { this.setState({ - msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}], addTask: null, continueDisabled: false, verifying: false, @@ -190,6 +186,11 @@ export default class PhoneNumbers extends React.Component { newPhoneNumber: "", newPhoneNumberCode: "", }); + const msisdns = [ + ...this.props.msisdns, + { address: this.state.verifyMsisdn, medium: "msisdn" }, + ]; + this.props.onMsisdnsChange(msisdns); }).catch((err) => { this.setState({continueDisabled: false}); if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { @@ -210,7 +211,7 @@ export default class PhoneNumbers extends React.Component { }; render() { - const existingPhoneElements = this.state.msisdns.map((p) => { + const existingPhoneElements = this.props.msisdns.map((p) => { return ; }); diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js index 4d18c1d355..c6ec826de6 100644 --- a/src/components/views/settings/discovery/EmailAddresses.js +++ b/src/components/views/settings/discovery/EmailAddresses.js @@ -23,7 +23,6 @@ import MatrixClientPeg from "../../../../MatrixClientPeg"; import sdk from '../../../../index'; import Modal from '../../../../Modal'; import AddThreepid from '../../../../AddThreepid'; -import { getThreepidBindStatus } from '../../../../boundThreepids'; /* TODO: Improve the UX for everything in here. @@ -187,27 +186,14 @@ export class EmailAddress extends React.Component { } export default class EmailAddresses extends React.Component { - constructor() { - super(); - - this.state = { - loaded: false, - emails: [], - }; - } - - async componentWillMount() { - const client = MatrixClientPeg.get(); - - const emails = await getThreepidBindStatus(client, 'email'); - - this.setState({ emails }); + static propTypes = { + emails: PropTypes.array.isRequired, } render() { let content; - if (this.state.emails.length > 0) { - content = this.state.emails.map((e) => { + if (this.props.emails.length > 0) { + content = this.props.emails.map((e) => { return ; }); } else { diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js index fdebac5d22..6d5c8ad3b4 100644 --- a/src/components/views/settings/discovery/PhoneNumbers.js +++ b/src/components/views/settings/discovery/PhoneNumbers.js @@ -23,7 +23,6 @@ import MatrixClientPeg from "../../../../MatrixClientPeg"; import sdk from '../../../../index'; import Modal from '../../../../Modal'; import AddThreepid from '../../../../AddThreepid'; -import { getThreepidBindStatus } from '../../../../boundThreepids'; /* TODO: Improve the UX for everything in here. @@ -206,27 +205,14 @@ export class PhoneNumber extends React.Component { } export default class PhoneNumbers extends React.Component { - constructor() { - super(); - - this.state = { - loaded: false, - msisdns: [], - }; - } - - async componentWillMount() { - const client = MatrixClientPeg.get(); - - const msisdns = await getThreepidBindStatus(client, 'msisdn'); - - this.setState({ msisdns }); + static propTypes = { + msisdns: PropTypes.array.isRequired, } render() { let content; - if (this.state.msisdns.length > 0) { - content = this.state.msisdns.map((e) => { + if (this.props.msisdns.length > 0) { + content = this.props.msisdns.map((e) => { return ; }); } else { diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 9c37730fc5..02f9cf3cd3 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -37,6 +37,7 @@ import {Service, startTermsFlow} from "../../../../../Terms"; import {SERVICE_TYPES} from "matrix-js-sdk"; import IdentityAuthClient from "../../../../../IdentityAuthClient"; import {abbreviateUrl} from "../../../../../utils/UrlUtils"; +import { getThreepidsWithBindStatus } from '../../../../../boundThreepids'; export default class GeneralUserSettingsTab extends React.Component { static propTypes = { @@ -58,17 +59,27 @@ export default class GeneralUserSettingsTab extends React.Component { // agreedUrls, // From the startTermsFlow callback // resolve, // Promise resolve function for startTermsFlow callback }, + emails: [], + msisdns: [], }; this.dispatcherRef = dis.register(this._onAction); } async componentWillMount() { - const serverRequiresIdServer = await MatrixClientPeg.get().doesServerRequireIdServerParam(); + const cli = MatrixClientPeg.get(); + + const serverRequiresIdServer = await cli.doesServerRequireIdServerParam(); this.setState({serverRequiresIdServer}); // Check to see if terms need accepting this._checkTerms(); + + // Need to get 3PIDs generally for Account section and possibly also for + // Discovery (assuming we have an IS and terms are agreed). + const threepids = await getThreepidsWithBindStatus(cli); + this.setState({ emails: threepids.filter((a) => a.medium === 'email') }); + this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') }); } componentWillUnmount() { @@ -82,6 +93,14 @@ export default class GeneralUserSettingsTab extends React.Component { } }; + _onEmailsChange = (emails) => { + this.setState({ emails }); + } + + _onMsisdnsChange = (msisdns) => { + this.setState({ msisdns }); + } + async _checkTerms() { if (!this.state.haveIdServer) { this.setState({idServerHasUnsignedTerms: false}); @@ -200,10 +219,16 @@ export default class GeneralUserSettingsTab extends React.Component { if (this.state.haveIdServer || this.state.serverRequiresIdServer === false) { threepidSection =
{_t("Email addresses")} - + {_t("Phone numbers")} - +
; } else if (this.state.serverRequiresIdServer === null) { threepidSection = ; @@ -279,10 +304,10 @@ export default class GeneralUserSettingsTab extends React.Component { const threepidSection = this.state.haveIdServer ?
{_t("Email addresses")} - + {_t("Phone numbers")} - +
: null; return ( From 0b7995dc11f06084540037201e3fad16214c26a0 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Sep 2019 13:36:10 +0100 Subject: [PATCH 395/413] Improve terms handling for 3PID state gathering This changes the 3PID state gathering (used in Settings) to ignore terms errors (no modals will be shown) on the assumption that other UX handles this case. --- src/IdentityAuthClient.js | 2 +- src/boundThreepids.js | 37 +++++++++++-------- .../tabs/user/GeneralUserSettingsTab.js | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 075ae93709..7cbad074bf 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -65,7 +65,7 @@ export default class IdentityAuthClient { } // Returns a promise that resolves to the access_token string from the IS - async getAccessToken(check=true) { + async getAccessToken({ check = true } = {}) { if (!this.authEnabled) { // The current IS doesn't support authentication return null; diff --git a/src/boundThreepids.js b/src/boundThreepids.js index 90a5bd2a6f..7f727d8e64 100644 --- a/src/boundThreepids.js +++ b/src/boundThreepids.js @@ -26,26 +26,31 @@ export async function getThreepidsWithBindStatus(client, filterMedium) { // Check bind status assuming we have an IS and terms are agreed if (threepids.length > 0 && client.getIdentityServerUrl()) { - // TODO: Handle terms agreement - // See https://github.com/vector-im/riot-web/issues/10522 - const authClient = new IdentityAuthClient(); - const identityAccessToken = await authClient.getAccessToken(); + try { + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken({ check: false }); - // Restructure for lookup query - const query = threepids.map(({ medium, address }) => [medium, address]); - const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); + // Restructure for lookup query + const query = threepids.map(({ medium, address }) => [medium, address]); + const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); - // Record which are already bound - for (const [medium, address, mxid] of lookupResults.threepids) { - if (mxid !== userId) { - continue; + // Record which are already bound + for (const [medium, address, mxid] of lookupResults.threepids) { + if (mxid !== userId) { + continue; + } + if (filterMedium && medium !== filterMedium) { + continue; + } + const threepid = threepids.find(e => e.medium === medium && e.address === address); + if (!threepid) continue; + threepid.bound = true; } - if (filterMedium && medium !== filterMedium) { - continue; + } catch (e) { + // Ignore terms errors here and assume other flows handle this + if (!(e.errcode === "M_TERMS_NOT_SIGNED")) { + throw e; } - const threepid = threepids.find(e => e.medium === medium && e.address === address); - if (!threepid) continue; - threepid.bound = true; } } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 02f9cf3cd3..f1ca314f13 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -110,7 +110,7 @@ export default class GeneralUserSettingsTab extends React.Component { // By starting the terms flow we get the logic for checking which terms the user has signed // for free. So we might as well use that for our own purposes. const authClient = new IdentityAuthClient(); - const idAccessToken = await authClient.getAccessToken(/*check=*/false); + const idAccessToken = await authClient.getAccessToken({ check: false }); startTermsFlow([new Service( SERVICE_TYPES.IS, MatrixClientPeg.get().getIdentityServerUrl(), From db33c138aa50d60a4c93e0c3eba08107e3fcf421 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Sep 2019 16:16:07 +0100 Subject: [PATCH 396/413] Update all 3PID state in Settings when IS changes This ensures we update all 3PID state (like bind status) whenever the IS changes. --- .../settings/discovery/EmailAddresses.js | 5 ++++ .../views/settings/discovery/PhoneNumbers.js | 5 ++++ .../tabs/user/GeneralUserSettingsTab.js | 24 ++++++++++++------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js index c6ec826de6..d6628f900a 100644 --- a/src/components/views/settings/discovery/EmailAddresses.js +++ b/src/components/views/settings/discovery/EmailAddresses.js @@ -58,6 +58,11 @@ export class EmailAddress extends React.Component { }; } + componentWillReceiveProps(nextProps) { + const { bound } = nextProps.email; + this.setState({ bound }); + } + async changeBinding({ bind, label, errorTitle }) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const { medium, address } = this.props.email; diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js index 6d5c8ad3b4..99a90f23fb 100644 --- a/src/components/views/settings/discovery/PhoneNumbers.js +++ b/src/components/views/settings/discovery/PhoneNumbers.js @@ -50,6 +50,11 @@ export class PhoneNumber extends React.Component { }; } + componentWillReceiveProps(nextProps) { + const { bound } = nextProps.msisdn; + this.setState({ bound }); + } + async changeBinding({ bind, label, errorTitle }) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const { medium, address } = this.props.msisdn; diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index f1ca314f13..b378db707a 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -72,14 +72,7 @@ export default class GeneralUserSettingsTab extends React.Component { const serverRequiresIdServer = await cli.doesServerRequireIdServerParam(); this.setState({serverRequiresIdServer}); - // Check to see if terms need accepting - this._checkTerms(); - - // Need to get 3PIDs generally for Account section and possibly also for - // Discovery (assuming we have an IS and terms are agreed). - const threepids = await getThreepidsWithBindStatus(cli); - this.setState({ emails: threepids.filter((a) => a.medium === 'email') }); - this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') }); + this._getThreepidState(); } componentWillUnmount() { @@ -89,7 +82,7 @@ export default class GeneralUserSettingsTab extends React.Component { _onAction = (payload) => { if (payload.action === 'id_server_changed') { this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); - this._checkTerms(); + this._getThreepidState(); } }; @@ -101,6 +94,19 @@ export default class GeneralUserSettingsTab extends React.Component { this.setState({ msisdns }); } + async _getThreepidState() { + const cli = MatrixClientPeg.get(); + + // Check to see if terms need accepting + this._checkTerms(); + + // Need to get 3PIDs generally for Account section and possibly also for + // Discovery (assuming we have an IS and terms are agreed). + const threepids = await getThreepidsWithBindStatus(cli); + this.setState({ emails: threepids.filter((a) => a.medium === 'email') }); + this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') }); + } + async _checkTerms() { if (!this.state.haveIdServer) { this.setState({idServerHasUnsignedTerms: false}); From c542ea555967f1f8a8bfcb4e4ffaf7c30b563a95 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 11 Sep 2019 16:55:45 +0100 Subject: [PATCH 397/413] Force boolean value --- src/boundThreepids.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boundThreepids.js b/src/boundThreepids.js index 7f727d8e64..3b32815913 100644 --- a/src/boundThreepids.js +++ b/src/boundThreepids.js @@ -25,7 +25,7 @@ export async function getThreepidsWithBindStatus(client, filterMedium) { } // Check bind status assuming we have an IS and terms are agreed - if (threepids.length > 0 && client.getIdentityServerUrl()) { + if (threepids.length > 0 && !!client.getIdentityServerUrl()) { try { const authClient = new IdentityAuthClient(); const identityAccessToken = await authClient.getAccessToken({ check: false }); From 0d28cd58404a80a828c2841c5aedf3af4eaef0bf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 11:52:39 +0100 Subject: [PATCH 398/413] RoomDirectory: show spinner if loading more results Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomDirectory.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index b85dc20b21..299216d022 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +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. @@ -140,6 +141,10 @@ module.exports = createReactClass({ getMoreRooms: function() { if (!MatrixClientPeg.get()) return Promise.resolve(); + this.setState({ + loading: true, + }); + const my_filter_string = this.state.filterString; const my_server = this.state.roomServer; // remember the next batch token when we sent the request @@ -554,15 +559,21 @@ module.exports = createReactClass({ let content; if (this.state.error) { content = this.state.error; - } else if (this.state.protocolsLoading || this.state.loading) { + } else if (this.state.protocolsLoading) { content = ; } else { const rows = (this.state.publicRooms || []).map(room => this.getRow(room)); // we still show the scrollpanel, at least for now, because // otherwise we don't fetch more because we don't get a fill // request from the scrollpanel because there isn't one + + let spinner; + if (this.state.loading) { + spinner = ; + } + let scrollpanel_content; - if (rows.length == 0) { + if (rows.length === 0 && !this.state.loading) { scrollpanel_content = { _t('No rooms to show') }; } else { scrollpanel_content = @@ -579,6 +590,7 @@ module.exports = createReactClass({ startAtBottom={false} > { scrollpanel_content } + { spinner } ; } From 980e9839ac8ded7187191500db4062c7d4791fa8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 16:02:37 +0100 Subject: [PATCH 399/413] Update Copyright --- src/components/structures/RoomDirectory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 299216d022..d3c65dceda 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -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. From e3643bf17abf2f88704aa0807ece25728e2e76e6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Sep 2019 18:12:52 +0100 Subject: [PATCH 400/413] EditMessageComposer: disable Save button until a change has been made Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/EditMessageComposer.js | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index d58279436d..58f1f75726 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -112,6 +112,10 @@ export default class EditMessageComposer extends React.Component { super(props, context); this.model = null; this._editorRef = null; + + this.state = { + changed: false, + }; } _setEditorRef = ref => { @@ -176,16 +180,16 @@ export default class EditMessageComposer extends React.Component { const editedEvent = this.props.editState.getEvent(); const editContent = createEditContent(this.model, editedEvent); const newContent = editContent["m.new_content"]; - if (!this._isModifiedOrSameAsOld(newContent)) { - return; + + if (this._isModifiedOrSameAsOld(newContent)) { + const roomId = editedEvent.getRoomId(); + this._cancelPreviousPendingEdit(); + this.context.matrixClient.sendMessage(roomId, editContent); } - const roomId = editedEvent.getRoomId(); - this._cancelPreviousPendingEdit(); - this.context.matrixClient.sendMessage(roomId, editContent); dis.dispatch({action: "edit_event", event: null}); dis.dispatch({action: 'focus_composer'}); - } + }; _cancelPreviousPendingEdit() { const originalEvent = this.props.editState.getEvent(); @@ -240,6 +244,16 @@ export default class EditMessageComposer extends React.Component { return caretPosition; } + _onChange = () => { + if (this.state.changed || !this._editorRef || !this._editorRef.isModified()) { + return; + } + + this.setState({ + changed: true, + }); + }; + render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
@@ -249,10 +263,13 @@ export default class EditMessageComposer extends React.Component { room={this._getRoom()} initialCaret={this.props.editState.getCaret()} label={_t("Edit message")} + onChange={this._onChange} />
{_t("Cancel")} - {_t("Save")} + + {_t("Save")} +
); } From 2ff98b7c1f99ef2b9ec09c878c1824b0791f9f83 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Sep 2019 08:51:27 +0100 Subject: [PATCH 401/413] clear up ambiguity by poor naming Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EditMessageComposer.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 58f1f75726..89aea64139 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -164,7 +164,7 @@ export default class EditMessageComposer extends React.Component { dis.dispatch({action: 'focus_composer'}); } - _isModifiedOrSameAsOld(newContent) { + _isContentModified(newContent) { // if nothing has changed then bail const oldContent = this.props.editState.getEvent().getContent(); if (!this._editorRef.isModified() || @@ -181,12 +181,14 @@ export default class EditMessageComposer extends React.Component { const editContent = createEditContent(this.model, editedEvent); const newContent = editContent["m.new_content"]; - if (this._isModifiedOrSameAsOld(newContent)) { + // If content is modified then send an updated event into the room + if (this._isContentModified(newContent)) { const roomId = editedEvent.getRoomId(); this._cancelPreviousPendingEdit(); this.context.matrixClient.sendMessage(roomId, editContent); } + // close the event editing and focus composer dis.dispatch({action: "edit_event", event: null}); dis.dispatch({action: 'focus_composer'}); }; From 792b70913c5d43ef3ea3b5bf17e4a2d323f09d31 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 17:29:52 +0100 Subject: [PATCH 402/413] invert and rename changed to saveDisabled for clarity Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EditMessageComposer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 89aea64139..a1d6fa618f 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -114,7 +114,7 @@ export default class EditMessageComposer extends React.Component { this._editorRef = null; this.state = { - changed: false, + saveDisabled: true, }; } @@ -247,12 +247,12 @@ export default class EditMessageComposer extends React.Component { } _onChange = () => { - if (this.state.changed || !this._editorRef || !this._editorRef.isModified()) { + if (!this.state.saveDisabled || !this._editorRef || !this._editorRef.isModified()) { return; } this.setState({ - changed: true, + saveDisabled: false, }); }; @@ -269,7 +269,7 @@ export default class EditMessageComposer extends React.Component { />
{_t("Cancel")} - + {_t("Save")}
From f1ea5ff6f39d831e61bb25dadf0889b55e572dae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 10:49:58 +0100 Subject: [PATCH 403/413] Login: don't assume supported flows, prevent login flash on SSO servers Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/Login.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 014fb4426d..ab76c990b6 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -93,7 +93,7 @@ module.exports = createReactClass({ // Phase of the overall login dialog. phase: PHASE_LOGIN, // The current login flow, such as password, SSO, etc. - currentFlow: "m.login.password", + currentFlow: null, // we need to load the flows from the server // We perform liveliness checks later, but for now suppress the errors. // We also track the server dead errors independently of the regular errors so @@ -372,6 +372,7 @@ module.exports = createReactClass({ this.setState({ busy: true, + currentFlow: null, // reset flow loginIncorrect: false, }); From 1c7d67e8b3ff652b2220e6867f2f72069557df47 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 11:01:06 +0100 Subject: [PATCH 404/413] fix test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/components/structures/auth/Login-test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js index 74451b922f..e79cf037d0 100644 --- a/test/components/structures/auth/Login-test.js +++ b/test/components/structures/auth/Login-test.js @@ -75,6 +75,11 @@ describe('Login', function() { const root = render(); + // Set non-empty flows & matrixClient to get past the loading spinner + root.setState({ + currentFlow: "m.login.password", + }); + const form = ReactTestUtils.findRenderedComponentWithType( root, sdk.getComponent('auth.PasswordLogin'), From bf30cfe6995799b997c705b2ce8bb49e015ffb11 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 11:20:03 +0100 Subject: [PATCH 405/413] Fix other test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/components/structures/auth/Login-test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js index e79cf037d0..6a7982dd47 100644 --- a/test/components/structures/auth/Login-test.js +++ b/test/components/structures/auth/Login-test.js @@ -55,6 +55,11 @@ describe('Login', function() { it('should show form with change server link', function() { const root = render(); + // Set non-empty flows & matrixClient to get past the loading spinner + root.setState({ + currentFlow: "m.login.password", + }); + const form = ReactTestUtils.findRenderedComponentWithType( root, sdk.getComponent('auth.PasswordLogin'), From 76e4363452f4d175b55b28f850d85bdfefd95624 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 12:20:36 +0100 Subject: [PATCH 406/413] Login: Add way to change HS from SSO Homeserver Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/Login.js | 8 ++- src/components/views/auth/PasswordLogin.js | 35 ++---------- src/components/views/auth/SignInToText.js | 62 ++++++++++++++++++++++ 3 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 src/components/views/auth/SignInToText.js diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ab76c990b6..594a484bde 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -566,6 +566,7 @@ module.exports = createReactClass({ }, _renderSsoStep: function(url) { + const SignInToText = sdk.getComponent('views.auth.SignInToText'); // XXX: This link does *not* have a target="_blank" because single sign-on relies on // redirecting the user back to a URI once they're logged in. On the web, this means // we use the same window and redirect back to riot. On electron, this actually @@ -574,9 +575,12 @@ module.exports = createReactClass({ // If this bug gets fixed, it will break SSO since it will open the SSO page in the // user's browser, let them log into their SSO provider, then redirect their browser // to vector://vector which, of course, will not work. - return ( + return ; }, render: function() { diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 59acf0a034..63e77a938d 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -31,6 +31,7 @@ export default class PasswordLogin extends React.Component { static propTypes = { onSubmit: PropTypes.func.isRequired, // fn(username, password) onError: PropTypes.func, + onEditServerDetailsClick: PropTypes.func, onForgotPasswordClick: PropTypes.func, // fn() initialUsername: PropTypes.string, initialPhoneCountry: PropTypes.string, @@ -257,6 +258,7 @@ export default class PasswordLogin extends React.Component { render() { const Field = sdk.getComponent('elements.Field'); + const SignInToText = sdk.getComponent('views.auth.SignInToText'); let forgotPasswordJsx; @@ -273,33 +275,6 @@ export default class PasswordLogin extends React.Component { ; } - let signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: this.props.serverConfig.hsName, - }); - if (this.props.serverConfig.hsNameIsDifferent) { - const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); - - signInToText = _t('Sign in to your Matrix account on ', {}, { - 'underlinedServerName': () => { - return ; - }, - }); - } - - let editLink = null; - if (this.props.onEditServerDetailsClick) { - editLink = - {_t('Change')} - ; - } - const pwFieldClass = classNames({ error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field }); @@ -342,10 +317,8 @@ export default class PasswordLogin extends React.Component { return (
-

- {signInToText} - {editLink} -

+
{loginType} {loginField} diff --git a/src/components/views/auth/SignInToText.js b/src/components/views/auth/SignInToText.js new file mode 100644 index 0000000000..edbe2fd661 --- /dev/null +++ b/src/components/views/auth/SignInToText.js @@ -0,0 +1,62 @@ +/* +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. +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 {_t} from "../../../languageHandler"; +import sdk from "../../../index"; +import PropTypes from "prop-types"; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; + +export default class SignInToText extends React.PureComponent { + static propTypes = { + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + onEditServerDetailsClick: PropTypes.func, + }; + + render() { + let signInToText = _t('Sign in to your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + + signInToText = _t('Sign in to your Matrix account on ', {}, { + 'underlinedServerName': () => { + return ; + }, + }); + } + + let editLink = null; + if (this.props.onEditServerDetailsClick) { + editLink = + {_t('Change')} + ; + } + + return

+ {signInToText} + {editLink} +

; + } +} From 370d9d83364991a710574bbdcc2f5db47c0749b3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 13:57:19 +0100 Subject: [PATCH 407/413] regen 18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 108122d7c9..2c38f918a0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1471,8 +1471,6 @@ "Username": "Username", "Phone": "Phone", "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", - "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", - "Sign in to your Matrix account on ": "Sign in to your Matrix account on ", "Sign in with": "Sign in with", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.", @@ -1508,6 +1506,8 @@ "Premium": "Premium", "Premium hosting for organisations Learn more": "Premium hosting for organisations Learn more", "Find other public servers or use a custom server": "Find other public servers or use a custom server", + "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", + "Sign in to your Matrix account on ": "Sign in to your Matrix account on ", "Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.", "Please install Chrome, Firefox, or Safari for the best experience.": "Please install Chrome, Firefox, or Safari for the best experience.", From 6f736e84079d8d3bdbb8e19bb89d2e021d0d4ed1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 17:34:29 +0100 Subject: [PATCH 408/413] Apply PR feedback Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/Login.js | 12 +++++++----- src/components/views/auth/SignInToText.js | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 594a484bde..0317462ebd 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -575,12 +575,14 @@ module.exports = createReactClass({ // If this bug gets fixed, it will break SSO since it will open the SSO page in the // user's browser, let them log into their SSO provider, then redirect their browser // to vector://vector which, of course, will not work. - return + ); }, render: function() { diff --git a/src/components/views/auth/SignInToText.js b/src/components/views/auth/SignInToText.js index edbe2fd661..a7acdc6705 100644 --- a/src/components/views/auth/SignInToText.js +++ b/src/components/views/auth/SignInToText.js @@ -1,5 +1,5 @@ /* -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. From a4a85dc54179cb03554daad1e76678aa8fe99906 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Sep 2019 23:02:52 +0100 Subject: [PATCH 409/413] Hide the change HS url button on SSO login flow if custom urls disabled Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/Login.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 0317462ebd..ad77ed49a5 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -567,6 +567,12 @@ module.exports = createReactClass({ _renderSsoStep: function(url) { const SignInToText = sdk.getComponent('views.auth.SignInToText'); + + let onEditServerDetailsClick = null; + // If custom URLs are allowed, wire up the server details edit link. + if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { + onEditServerDetailsClick = this.onEditServerDetailsClick; + } // XXX: This link does *not* have a target="_blank" because single sign-on relies on // redirecting the user back to a URI once they're logged in. On the web, this means // we use the same window and redirect back to riot. On electron, this actually @@ -578,7 +584,7 @@ module.exports = createReactClass({ return (
+ onEditServerDetailsClick={onEditServerDetailsClick} /> { _t('Sign in with single sign-on') }
From 4ae4e68967e3d7a8db78b00e581cd773dc8c4de9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Sep 2019 11:11:32 +0200 Subject: [PATCH 410/413] make explore button and filter field equal width --- res/css/structures/_LeftPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index f83195f847..ac17083621 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -136,7 +136,7 @@ limitations under the License. } .mx_LeftPanel_explore { - flex: 0 0 40%; + flex: 0 0 50%; overflow: hidden; transition: flex-basis 0.2s; box-sizing: border-box; From 7c970787648be9a6004917a116e0c41382ec47b2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Sep 2019 11:12:06 +0200 Subject: [PATCH 411/413] always show clear button in search box when focused --- src/components/structures/SearchBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 4d68ff4e96..d495fffbc9 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -126,7 +126,7 @@ module.exports = createReactClass({ if (this.props.collapsed) { return null; } - const clearButton = this.state.searchTerm.length > 0 ? + const clearButton = !this.state.blurred ? ( {this._clearSearch("button"); } }> From 95060d4e95c9f2eb73e9ca81334d1ee8f47f1181 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Sep 2019 11:27:20 +0200 Subject: [PATCH 412/413] reduce vertical padding around explore/filter --- res/css/structures/_LeftPanel.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index ac17083621..85fdfa092d 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -132,6 +132,7 @@ limitations under the License. .mx_SearchBox { flex: 1 1 0; min-width: 0; + margin: 4px 9px 1px 9px; } } @@ -147,8 +148,7 @@ limitations under the License. .mx_AccessibleButton { font-size: 14px; - margin: 9px; - margin-right: 0; + margin: 4px 0 1px 9px; padding: 9px; padding-left: 42px; font-weight: 600; From cc67742fa9d41ec50b69e64a2f740420458d0b91 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Sep 2019 12:24:05 +0200 Subject: [PATCH 413/413] undo whitespace setting, accessible button is used in too many places to make this a safe assumption --- res/css/views/elements/_AccessibleButton.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 5ca5d002ba..0c081ec0d5 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -16,7 +16,6 @@ limitations under the License. .mx_AccessibleButton { cursor: pointer; - white-space: nowrap; } .mx_AccessibleButton:focus {