Merge branch 'develop' into gsouquet/switch-rooms
commit
282b9f9e0f
110
CHANGELOG.md
110
CHANGELOG.md
|
@ -1,3 +1,113 @@
|
||||||
|
Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0)
|
||||||
|
|
||||||
|
## Security notice
|
||||||
|
|
||||||
|
matrix-react-sdk 3.21.0 fixes a low severity issue (GHSA-8796-gc9j-63rv)
|
||||||
|
related to file upload. When uploading a file, the local file preview can lead
|
||||||
|
to execution of scripts embedded in the uploaded file, but only after several
|
||||||
|
user interactions to open the preview in a separate tab. This only impacts the
|
||||||
|
local user while in the process of uploading. It cannot be exploited remotely
|
||||||
|
or by other users. Thanks to [Muhammad Zaid Ghifari](https://github.com/MR-ZHEEV)
|
||||||
|
for responsibly disclosing this via Matrix's Security Disclosure Policy.
|
||||||
|
|
||||||
|
## All changes
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.0.0
|
||||||
|
* [Release] Add missing space on beta feedback dialog
|
||||||
|
[\#6019](https://github.com/matrix-org/matrix-react-sdk/pull/6019)
|
||||||
|
* [Release] Add feedback mechanism for beta features, namely Spaces
|
||||||
|
[\#6013](https://github.com/matrix-org/matrix-react-sdk/pull/6013)
|
||||||
|
* Add feedback mechanism for beta features, namely Spaces
|
||||||
|
[\#6012](https://github.com/matrix-org/matrix-react-sdk/pull/6012)
|
||||||
|
|
||||||
|
Changes in [3.21.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0-rc.1) (2021-05-11)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0...v3.21.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.0.0-rc.1
|
||||||
|
* Add disclaimer about subspaces being experimental in add existing dialog
|
||||||
|
[\#5978](https://github.com/matrix-org/matrix-react-sdk/pull/5978)
|
||||||
|
* Spaces Beta release
|
||||||
|
[\#5933](https://github.com/matrix-org/matrix-react-sdk/pull/5933)
|
||||||
|
* Improve permissions error when adding new server to room directory
|
||||||
|
[\#6009](https://github.com/matrix-org/matrix-react-sdk/pull/6009)
|
||||||
|
* Allow user to progress through space creation & setup using Enter
|
||||||
|
[\#6006](https://github.com/matrix-org/matrix-react-sdk/pull/6006)
|
||||||
|
* Upgrade sanitize types
|
||||||
|
[\#6008](https://github.com/matrix-org/matrix-react-sdk/pull/6008)
|
||||||
|
* Upgrade `cheerio` and resolve type errors
|
||||||
|
[\#6007](https://github.com/matrix-org/matrix-react-sdk/pull/6007)
|
||||||
|
* Add slash commands support to edit message composer
|
||||||
|
[\#5865](https://github.com/matrix-org/matrix-react-sdk/pull/5865)
|
||||||
|
* Fix the two todays problem
|
||||||
|
[\#5940](https://github.com/matrix-org/matrix-react-sdk/pull/5940)
|
||||||
|
* Switch the Home Space out for an All rooms space
|
||||||
|
[\#5969](https://github.com/matrix-org/matrix-react-sdk/pull/5969)
|
||||||
|
* Show device ID in UserInfo when there is no device name
|
||||||
|
[\#5985](https://github.com/matrix-org/matrix-react-sdk/pull/5985)
|
||||||
|
* Switch back to release version of `sanitize-html`
|
||||||
|
[\#6005](https://github.com/matrix-org/matrix-react-sdk/pull/6005)
|
||||||
|
* Bump hosted-git-info from 2.8.8 to 2.8.9
|
||||||
|
[\#5998](https://github.com/matrix-org/matrix-react-sdk/pull/5998)
|
||||||
|
* Don't use the event's metadata to calc the scale of an image
|
||||||
|
[\#5982](https://github.com/matrix-org/matrix-react-sdk/pull/5982)
|
||||||
|
* Adjust MIME type of upload confirmation if needed
|
||||||
|
[\#5981](https://github.com/matrix-org/matrix-react-sdk/pull/5981)
|
||||||
|
* Forbid redaction of encryption events
|
||||||
|
[\#5991](https://github.com/matrix-org/matrix-react-sdk/pull/5991)
|
||||||
|
* Fix voice message playback being squished up against send button
|
||||||
|
[\#5988](https://github.com/matrix-org/matrix-react-sdk/pull/5988)
|
||||||
|
* Improve style of notification badges on the space panel
|
||||||
|
[\#5983](https://github.com/matrix-org/matrix-react-sdk/pull/5983)
|
||||||
|
* Add dev dependency for parse5 typings
|
||||||
|
[\#5990](https://github.com/matrix-org/matrix-react-sdk/pull/5990)
|
||||||
|
* Iterate Spaces admin UX around room management
|
||||||
|
[\#5977](https://github.com/matrix-org/matrix-react-sdk/pull/5977)
|
||||||
|
* Guard all isSpaceRoom calls behind the labs flag
|
||||||
|
[\#5979](https://github.com/matrix-org/matrix-react-sdk/pull/5979)
|
||||||
|
* Bump lodash from 4.17.20 to 4.17.21
|
||||||
|
[\#5986](https://github.com/matrix-org/matrix-react-sdk/pull/5986)
|
||||||
|
* Bump lodash from 4.17.19 to 4.17.21 in /test/end-to-end-tests
|
||||||
|
[\#5987](https://github.com/matrix-org/matrix-react-sdk/pull/5987)
|
||||||
|
* Bump ua-parser-js from 0.7.23 to 0.7.28
|
||||||
|
[\#5984](https://github.com/matrix-org/matrix-react-sdk/pull/5984)
|
||||||
|
* Update visual style of plain files in the timeline
|
||||||
|
[\#5971](https://github.com/matrix-org/matrix-react-sdk/pull/5971)
|
||||||
|
* Support for multiple streams (not MSC3077)
|
||||||
|
[\#5833](https://github.com/matrix-org/matrix-react-sdk/pull/5833)
|
||||||
|
* Update space ordering behaviour to match updates in MSC
|
||||||
|
[\#5963](https://github.com/matrix-org/matrix-react-sdk/pull/5963)
|
||||||
|
* Improve performance of search all spaces and space switching
|
||||||
|
[\#5976](https://github.com/matrix-org/matrix-react-sdk/pull/5976)
|
||||||
|
* Update colours and sizing for voice messages
|
||||||
|
[\#5970](https://github.com/matrix-org/matrix-react-sdk/pull/5970)
|
||||||
|
* Update link to Android SDK
|
||||||
|
[\#5973](https://github.com/matrix-org/matrix-react-sdk/pull/5973)
|
||||||
|
* Add cleanup functions for image view
|
||||||
|
[\#5962](https://github.com/matrix-org/matrix-react-sdk/pull/5962)
|
||||||
|
* Add a note about sharing your IP in P2P calls
|
||||||
|
[\#5961](https://github.com/matrix-org/matrix-react-sdk/pull/5961)
|
||||||
|
* Only aggregate DM notifications on the Space Panel in the Home Space
|
||||||
|
[\#5968](https://github.com/matrix-org/matrix-react-sdk/pull/5968)
|
||||||
|
* Add retry mechanism and progress bar to add existing to space dialog
|
||||||
|
[\#5975](https://github.com/matrix-org/matrix-react-sdk/pull/5975)
|
||||||
|
* Warn on access token reveal
|
||||||
|
[\#5755](https://github.com/matrix-org/matrix-react-sdk/pull/5755)
|
||||||
|
* Fix newly joined room appearing under the wrong space
|
||||||
|
[\#5945](https://github.com/matrix-org/matrix-react-sdk/pull/5945)
|
||||||
|
* Early rendering for voice messages in the timeline
|
||||||
|
[\#5955](https://github.com/matrix-org/matrix-react-sdk/pull/5955)
|
||||||
|
* Calculate the real waveform in the Playback class for voice messages
|
||||||
|
[\#5956](https://github.com/matrix-org/matrix-react-sdk/pull/5956)
|
||||||
|
* Don't recurse on arrayFastResample
|
||||||
|
[\#5957](https://github.com/matrix-org/matrix-react-sdk/pull/5957)
|
||||||
|
* Support a dark theme for voice messages
|
||||||
|
[\#5958](https://github.com/matrix-org/matrix-react-sdk/pull/5958)
|
||||||
|
* Handle no/blocked microphones in voice messages
|
||||||
|
[\#5959](https://github.com/matrix-org/matrix-react-sdk/pull/5959)
|
||||||
|
|
||||||
Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10)
|
Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.20.0",
|
"version": "3.21.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
"matrix-widget-api": "^0.1.0-beta.13",
|
"matrix-widget-api": "^0.1.0-beta.14",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
|
|
|
@ -54,7 +54,8 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
||||||
.mx_BaseAvatar {
|
// we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
|
||||||
|
.mx_DecoratedRoomAvatar {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +76,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpace_section_spaces {
|
.mx_AddExistingToSpace_section_spaces {
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_BaseAvatar_image {
|
.mx_BaseAvatar_image {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
@ -105,6 +110,90 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_footer {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
|
||||||
|
.mx_ProgressBar {
|
||||||
|
height: 8px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@mixin ProgressBarBorderRadius 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_progressText {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_error {
|
||||||
|
padding-left: 12px;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_errorHeading {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $notice-primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_errorCaption {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
display: inline-block;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_primary {
|
||||||
|
padding: 8px 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpace_retryButton {
|
||||||
|
margin-left: 12px;
|
||||||
|
padding-left: 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog {
|
.mx_AddExistingToSpaceDialog {
|
||||||
|
@ -189,88 +278,4 @@ limitations under the License.
|
||||||
.mx_AddExistingToSpace {
|
.mx_AddExistingToSpace {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_footer {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
flex-grow: 1;
|
|
||||||
font-size: $font-12px;
|
|
||||||
line-height: $font-15px;
|
|
||||||
color: $secondary-fg-color;
|
|
||||||
|
|
||||||
.mx_ProgressBar {
|
|
||||||
height: 8px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
@mixin ProgressBarBorderRadius 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_progressText {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: $font-15px;
|
|
||||||
line-height: $font-24px;
|
|
||||||
color: $primary-fg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
> * {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_error {
|
|
||||||
padding-left: 12px;
|
|
||||||
|
|
||||||
> img {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_errorHeading {
|
|
||||||
font-weight: $font-semi-bold;
|
|
||||||
font-size: $font-15px;
|
|
||||||
line-height: $font-18px;
|
|
||||||
color: $notice-primary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_errorCaption {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: $font-12px;
|
|
||||||
line-height: $font-15px;
|
|
||||||
color: $primary-fg-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
|
||||||
display: inline-block;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_primary {
|
|
||||||
padding: 8px 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_retryButton {
|
|
||||||
margin-left: 12px;
|
|
||||||
padding-left: 24px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
background-color: $primary-fg-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-image: url('$(res)/img/element-icons/retry.svg');
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_link {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,8 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ReactionsRow_addReactionButton {
|
.mx_ReactionsRow_addReactionButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: none; // show on hover of the .mx_EventTile
|
display: inline-block;
|
||||||
|
visibility: hidden; // show on hover of the .mx_EventTile
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -39,7 +40,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_ReactionsRow_addReactionButton_active {
|
&.mx_ReactionsRow_addReactionButton_active {
|
||||||
display: inline-block; // keep showing whilst the context menu is shown
|
visibility: visible; // keep showing whilst the context menu is shown
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover, &.mx_ReactionsRow_addReactionButton_active {
|
&:hover, &.mx_ReactionsRow_addReactionButton_active {
|
||||||
|
@ -51,7 +52,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_ReactionsRow_addReactionButton {
|
.mx_EventTile:hover .mx_ReactionsRow_addReactionButton {
|
||||||
display: inline-block;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReactionsRow_showAll {
|
.mx_ReactionsRow_showAll {
|
||||||
|
|
|
@ -98,7 +98,7 @@ limitations under the License.
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 32px;
|
border-radius: 8px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -114,6 +114,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist_auxButton:hover,
|
||||||
|
.mx_RoomSublist_menuButton:hover {
|
||||||
|
background: $roomlist-button-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
// Hide the menu button by default
|
// Hide the menu button by default
|
||||||
.mx_RoomSublist_menuButton {
|
.mx_RoomSublist_menuButton {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|
|
@ -40,6 +40,8 @@ export function eventTriggersUnreadCount(ev) {
|
||||||
return false;
|
return false;
|
||||||
} else if (ev.getType() == 'm.room.server_acl') {
|
} else if (ev.getType() == 'm.room.server_acl') {
|
||||||
return false;
|
return false;
|
||||||
|
} else if (ev.isRedacted()) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return haveTileForEvent(ev);
|
return haveTileForEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,7 +167,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
|
||||||
const onKeyDownHandler = useCallback((ev) => {
|
const onKeyDownHandler = useCallback((ev) => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
// Don't interfere with input default keydown behaviour
|
// Don't interfere with input default keydown behaviour
|
||||||
if (handleHomeEnd && ev.target.tagName !== "INPUT") {
|
if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
||||||
// check if we actually have any items
|
// check if we actually have any items
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.HOME:
|
case Key.HOME:
|
||||||
|
|
|
@ -26,6 +26,8 @@ import EmojiProvider from './EmojiProvider';
|
||||||
import NotifProvider from './NotifProvider';
|
import NotifProvider from './NotifProvider';
|
||||||
import {timeout} from "../utils/promise";
|
import {timeout} from "../utils/promise";
|
||||||
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
|
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import SpaceProvider from "./SpaceProvider";
|
||||||
|
|
||||||
export interface ISelectionRange {
|
export interface ISelectionRange {
|
||||||
beginning?: boolean; // whether the selection is in the first block of the editor or not
|
beginning?: boolean; // whether the selection is in the first block of the editor or not
|
||||||
|
@ -56,6 +58,11 @@ const PROVIDERS = [
|
||||||
DuckDuckGoProvider,
|
DuckDuckGoProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
|
||||||
|
if (SettingsStore.getValue("feature_spaces")) {
|
||||||
|
PROVIDERS.push(SpaceProvider);
|
||||||
|
}
|
||||||
|
|
||||||
// Providers will get rejected if they take longer than this.
|
// Providers will get rejected if they take longer than this.
|
||||||
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
|
||||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2017, 2018, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,17 +16,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
import {uniqBy, sortBy} from "lodash";
|
||||||
import Room from "matrix-js-sdk/src/models/room";
|
import Room from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import * as sdk from '../index';
|
|
||||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
import {uniqBy, sortBy} from "lodash";
|
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
|
||||||
const ROOM_REGEX = /\B#\S*/g;
|
const ROOM_REGEX = /\B#\S*/g;
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomProvider extends AutocompleteProvider {
|
export default class RoomProvider extends AutocompleteProvider {
|
||||||
matcher: QueryMatcher<Room>;
|
protected matcher: QueryMatcher<Room>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ROOM_REGEX);
|
super(ROOM_REGEX);
|
||||||
|
@ -58,20 +59,28 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getRooms() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
let rooms = cli.getVisibleRooms();
|
||||||
|
|
||||||
|
if (SettingsStore.getValue("feature_spaces")) {
|
||||||
|
rooms = rooms.filter(r => !r.isSpaceRoom());
|
||||||
|
}
|
||||||
|
|
||||||
|
return rooms;
|
||||||
|
}
|
||||||
|
|
||||||
async getCompletions(
|
async getCompletions(
|
||||||
query: string,
|
query: string,
|
||||||
selection: ISelectionRange,
|
selection: ISelectionRange,
|
||||||
force = false,
|
force = false,
|
||||||
limit = -1,
|
limit = -1,
|
||||||
): Promise<ICompletion[]> {
|
): Promise<ICompletion[]> {
|
||||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
// the only reason we need to do this is because Fuse only matches on properties
|
// the only reason we need to do this is because Fuse only matches on properties
|
||||||
let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => {
|
let matcherObjects = this.getRooms().reduce((aliases, room) => {
|
||||||
if (room.getCanonicalAlias()) {
|
if (room.getCanonicalAlias()) {
|
||||||
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
|
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
|
||||||
}
|
}
|
||||||
|
@ -115,7 +124,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4);
|
}).filter((completion) => !!completion.completion && completion.completion.length > 0);
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
|
import RoomProvider from "./RoomProvider";
|
||||||
|
|
||||||
|
export default class SpaceProvider extends RoomProvider {
|
||||||
|
protected getRooms() {
|
||||||
|
return MatrixClientPeg.get().getVisibleRooms().filter(r => r.isSpaceRoom());
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return _t("Spaces");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
|
||||||
|
role="listbox"
|
||||||
|
aria-label={_t("Space Autocomplete")}
|
||||||
|
>
|
||||||
|
{ completions }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -222,10 +222,12 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
// don't let keyboard handling escape the context menu
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
if (!this.props.managed) {
|
if (!this.props.managed) {
|
||||||
if (ev.key === Key.ESCAPE) {
|
if (ev.key === Key.ESCAPE) {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -258,7 +260,6 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
if (handled) {
|
if (handled) {
|
||||||
// consume all other keys in context menu
|
// consume all other keys in context menu
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,6 +50,9 @@ class FilePanel extends React.Component {
|
||||||
if (room?.roomId !== this.props?.roomId) return;
|
if (room?.roomId !== this.props?.roomId) return;
|
||||||
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
client.decryptEventIfNeeded(ev);
|
||||||
|
|
||||||
if (ev.isBeingDecrypted()) {
|
if (ev.isBeingDecrypted()) {
|
||||||
this.decryptingEvents.add(ev.getId());
|
this.decryptingEvents.add(ev.getId());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -27,7 +27,7 @@ import CallMediaHandler from '../../CallMediaHandler';
|
||||||
import { fixupColorFonts } from '../../utils/FontManager';
|
import { fixupColorFonts } from '../../utils/FontManager';
|
||||||
import * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
|
import { IMatrixClientCreds } from '../../MatrixClientPeg';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
import TagOrderActions from '../../actions/TagOrderActions';
|
import TagOrderActions from '../../actions/TagOrderActions';
|
||||||
|
@ -219,16 +219,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Child components assume that the client peg will not be null, so give them some
|
|
||||||
// sort of assurance here by only allowing a re-render if the client is truthy.
|
|
||||||
//
|
|
||||||
// This is required because `LoggedInView` maintains its own state and if this state
|
|
||||||
// updates after the client peg has been made null (during logout), then it will
|
|
||||||
// attempt to re-render and the children will throw errors.
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
return Boolean(MatrixClientPeg.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
canResetTimelineInRoom = (roomId) => {
|
canResetTimelineInRoom = (roomId) => {
|
||||||
if (!this._roomView.current) {
|
if (!this._roomView.current) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -473,7 +473,7 @@ export default class MessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get _roomHasPendingEdit() {
|
get _roomHasPendingEdit() {
|
||||||
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getEventTiles() {
|
_getEventTiles() {
|
||||||
|
|
|
@ -811,7 +811,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEvent = (ev) => {
|
private onEvent = (ev) => {
|
||||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return;
|
||||||
this.handleEffects(ev);
|
this.handleEffects(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -52,8 +52,6 @@ import {useStateToggle} from "../../hooks/useStateToggle";
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
import FacePile from "../views/elements/FacePile";
|
import FacePile from "../views/elements/FacePile";
|
||||||
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||||
import {sleep} from "../../utils/promise";
|
|
||||||
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
|
|
||||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
|
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
|
||||||
import IconizedContextMenu, {
|
import IconizedContextMenu, {
|
||||||
IconizedContextMenuOption,
|
IconizedContextMenuOption,
|
||||||
|
@ -78,6 +76,7 @@ interface IProps {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
phase: Phase;
|
phase: Phase;
|
||||||
|
createdRooms?: boolean; // internal state for the creation wizard
|
||||||
showRightPanel: boolean;
|
showRightPanel: boolean;
|
||||||
myMembership: string;
|
myMembership: string;
|
||||||
}
|
}
|
||||||
|
@ -461,7 +460,8 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||||
setError("");
|
setError("");
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => {
|
const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean);
|
||||||
|
await Promise.all(filteredRoomNames.map(name => {
|
||||||
return createRoom({
|
return createRoom({
|
||||||
createOpts: {
|
createOpts: {
|
||||||
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
|
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
|
||||||
|
@ -474,7 +474,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||||
parentSpace: space,
|
parentSpace: space,
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
onFinished();
|
onFinished(filteredRoomNames.length > 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to create initial space rooms", e);
|
console.error("Failed to create initial space rooms", e);
|
||||||
setError(_t("Failed to create initial space rooms"));
|
setError(_t("Failed to create initial space rooms"));
|
||||||
|
@ -484,7 +484,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||||
|
|
||||||
let onClick = (ev) => {
|
let onClick = (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
onFinished();
|
onFinished(false);
|
||||||
};
|
};
|
||||||
let buttonLabel = _t("Skip for now");
|
let buttonLabel = _t("Skip for now");
|
||||||
if (roomNames.some(name => name.trim())) {
|
if (roomNames.some(name => name.trim())) {
|
||||||
|
@ -517,39 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
||||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
|
||||||
|
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
let onClick = onFinished;
|
|
||||||
let buttonLabel = _t("Skip for now");
|
|
||||||
if (selectedToAdd.size > 0) {
|
|
||||||
onClick = async () => {
|
|
||||||
setBusy(true);
|
|
||||||
|
|
||||||
for (const room of selectedToAdd) {
|
|
||||||
const via = calculateRoomVia(room);
|
|
||||||
try {
|
|
||||||
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
|
|
||||||
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
|
||||||
await sleep(e.data.retry_after_ms);
|
|
||||||
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to add rooms to space", e);
|
|
||||||
setError(_t("Failed to add rooms to space"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBusy(false);
|
|
||||||
};
|
|
||||||
buttonLabel = busy ? _t("Adding...") : _t("Add");
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<h1>{ _t("What do you want to organise?") }</h1>
|
<h1>{ _t("What do you want to organise?") }</h1>
|
||||||
<div className="mx_SpaceRoomView_description">
|
<div className="mx_SpaceRoomView_description">
|
||||||
|
@ -557,35 +524,24 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
||||||
"no one will be informed. You can add more later.") }
|
"no one will be informed. You can add more later.") }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
|
||||||
|
|
||||||
<AddExistingToSpace
|
<AddExistingToSpace
|
||||||
space={space}
|
space={space}
|
||||||
selected={selectedToAdd}
|
emptySelectionButton={
|
||||||
onChange={(checked, room) => {
|
<AccessibleButton kind="primary" onClick={onFinished}>
|
||||||
if (checked) {
|
{ _t("Skip for now") }
|
||||||
selectedToAdd.add(room);
|
</AccessibleButton>
|
||||||
} else {
|
|
||||||
selectedToAdd.delete(room);
|
|
||||||
}
|
}
|
||||||
setSelectedToAdd(new Set(selectedToAdd));
|
onFinished={onFinished}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mx_SpaceRoomView_buttons">
|
<div className="mx_SpaceRoomView_buttons">
|
||||||
<AccessibleButton
|
|
||||||
kind="primary"
|
|
||||||
disabled={busy}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{ buttonLabel }
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
</div>
|
||||||
<SpaceFeedbackPrompt />
|
<SpaceFeedbackPrompt />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => {
|
const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRooms }) => {
|
||||||
return <div className="mx_SpaceRoomView_publicShare">
|
return <div className="mx_SpaceRoomView_publicShare">
|
||||||
<h1>{ _t("Share %(name)s", {
|
<h1>{ _t("Share %(name)s", {
|
||||||
name: justCreatedOpts?.createOpts?.name || space.name,
|
name: justCreatedOpts?.createOpts?.name || space.name,
|
||||||
|
@ -598,7 +554,7 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => {
|
||||||
|
|
||||||
<div className="mx_SpaceRoomView_buttons">
|
<div className="mx_SpaceRoomView_buttons">
|
||||||
<AccessibleButton kind="primary" onClick={onFinished}>
|
<AccessibleButton kind="primary" onClick={onFinished}>
|
||||||
{ _t("Go to my first room") }
|
{ createdRooms ? _t("Go to my first room") : _t("Go to my space") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
<SpaceFeedbackPrompt />
|
<SpaceFeedbackPrompt />
|
||||||
|
@ -891,13 +847,14 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||||
_t("Let's create a room for each of them.") + "\n" +
|
_t("Let's create a room for each of them.") + "\n" +
|
||||||
_t("You can add more later too, including already existing ones.")
|
_t("You can add more later too, including already existing ones.")
|
||||||
}
|
}
|
||||||
onFinished={() => this.setState({ phase: Phase.PublicShare })}
|
onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.PublicShare, createdRooms })}
|
||||||
/>;
|
/>;
|
||||||
case Phase.PublicShare:
|
case Phase.PublicShare:
|
||||||
return <SpaceSetupPublicShare
|
return <SpaceSetupPublicShare
|
||||||
justCreatedOpts={this.props.justCreatedOpts}
|
justCreatedOpts={this.props.justCreatedOpts}
|
||||||
space={this.props.space}
|
space={this.props.space}
|
||||||
onFinished={this.goToFirstRoom}
|
onFinished={this.goToFirstRoom}
|
||||||
|
createdRooms={this.state.createdRooms}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
case Phase.PrivateScope:
|
case Phase.PrivateScope:
|
||||||
|
@ -919,7 +876,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||||
title={_t("What projects are you working on?")}
|
title={_t("What projects are you working on?")}
|
||||||
description={_t("We'll create rooms for each of them. " +
|
description={_t("We'll create rooms for each of them. " +
|
||||||
"You can add more later too, including already existing ones.")}
|
"You can add more later too, including already existing ones.")}
|
||||||
onFinished={() => this.setState({ phase: Phase.Landing })}
|
onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.Landing, createdRooms })}
|
||||||
/>;
|
/>;
|
||||||
case Phase.PrivateExistingRooms:
|
case Phase.PrivateExistingRooms:
|
||||||
return <SpaceAddExistingRooms
|
return <SpaceAddExistingRooms
|
||||||
|
|
|
@ -1149,9 +1149,8 @@ class TimelinePanel extends React.Component {
|
||||||
arrayFastClone(events)
|
arrayFastClone(events)
|
||||||
.reverse()
|
.reverse()
|
||||||
.forEach(event => {
|
.forEach(event => {
|
||||||
if (event.shouldAttemptDecryption()) {
|
const client = MatrixClientPeg.get();
|
||||||
event.attemptDecryption(MatrixClientPeg.get()._crypto);
|
client.decryptEventIfNeeded(event);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
|
const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
|
||||||
|
|
|
@ -20,7 +20,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { User } from "matrix-js-sdk/src/models/user";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import { TagID } from '../../../stores/room-list/models';
|
|
||||||
import RoomAvatar from "./RoomAvatar";
|
import RoomAvatar from "./RoomAvatar";
|
||||||
import NotificationBadge from '../rooms/NotificationBadge';
|
import NotificationBadge from '../rooms/NotificationBadge';
|
||||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
|
@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
avatarSize: number;
|
avatarSize: number;
|
||||||
tag: TagID;
|
|
||||||
displayBadge?: boolean;
|
displayBadge?: boolean;
|
||||||
forceCount?: boolean;
|
forceCount?: boolean;
|
||||||
oobData?: object;
|
oobData?: object;
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useContext, useMemo, useState} from "react";
|
import React, {ReactNode, useContext, useMemo, useState} from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
|
@ -37,6 +37,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||||
import ProgressBar from "../elements/ProgressBar";
|
import ProgressBar from "../elements/ProgressBar";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||||
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -46,7 +47,10 @@ interface IProps extends IDialogProps {
|
||||||
|
|
||||||
const Entry = ({ room, checked, onChange }) => {
|
const Entry = ({ room, checked, onChange }) => {
|
||||||
return <label className="mx_AddExistingToSpace_entry">
|
return <label className="mx_AddExistingToSpace_entry">
|
||||||
<RoomAvatar room={room} height={32} width={32} />
|
{ room?.isSpaceRoom()
|
||||||
|
? <RoomAvatar room={room} height={32} width={32} />
|
||||||
|
: <DecoratedRoomAvatar room={room} avatarSize={32} />
|
||||||
|
}
|
||||||
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
|
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
|
||||||
<StyledCheckbox
|
<StyledCheckbox
|
||||||
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
||||||
|
@ -58,14 +62,23 @@ const Entry = ({ room, checked, onChange }) => {
|
||||||
|
|
||||||
interface IAddExistingToSpaceProps {
|
interface IAddExistingToSpaceProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
selected: Set<Room>;
|
footerPrompt?: ReactNode;
|
||||||
onChange(checked: boolean, room: Room): void;
|
emptySelectionButton?: ReactNode;
|
||||||
|
onFinished(added: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => {
|
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
|
space,
|
||||||
|
footerPrompt,
|
||||||
|
emptySelectionButton,
|
||||||
|
onFinished,
|
||||||
|
}) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
|
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
|
||||||
|
|
||||||
|
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||||
|
const [progress, setProgress] = useState<number>(null);
|
||||||
|
const [error, setError] = useState<Error>(null);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const lcQuery = query.toLowerCase();
|
const lcQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
@ -93,120 +106,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
|
||||||
return arr;
|
return arr;
|
||||||
}, [[], [], []]);
|
}, [[], [], []]);
|
||||||
|
|
||||||
return <div className="mx_AddExistingToSpace">
|
|
||||||
<SearchBox
|
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
|
||||||
placeholder={ _t("Filter your rooms and spaces") }
|
|
||||||
onSearch={setQuery}
|
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
|
||||||
/>
|
|
||||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
|
||||||
{ rooms.length > 0 ? (
|
|
||||||
<div className="mx_AddExistingToSpace_section">
|
|
||||||
<h3>{ _t("Rooms") }</h3>
|
|
||||||
{ rooms.map(room => {
|
|
||||||
return <Entry
|
|
||||||
key={room.roomId}
|
|
||||||
room={room}
|
|
||||||
checked={selected.has(room)}
|
|
||||||
onChange={onChange ? (checked) => {
|
|
||||||
onChange(checked, room);
|
|
||||||
} : null}
|
|
||||||
/>;
|
|
||||||
}) }
|
|
||||||
</div>
|
|
||||||
) : undefined }
|
|
||||||
|
|
||||||
{ spaces.length > 0 ? (
|
|
||||||
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
|
|
||||||
<h3>{ _t("Spaces") }</h3>
|
|
||||||
<div className="mx_AddExistingToSpace_section_experimental">
|
|
||||||
<div>{ _t("Feeling experimental?") }</div>
|
|
||||||
<div>{ _t("You can add existing spaces to a space.") }</div>
|
|
||||||
</div>
|
|
||||||
{ spaces.map(space => {
|
|
||||||
return <Entry
|
|
||||||
key={space.roomId}
|
|
||||||
room={space}
|
|
||||||
checked={selected.has(space)}
|
|
||||||
onChange={onChange ? (checked) => {
|
|
||||||
onChange(checked, space);
|
|
||||||
} : null}
|
|
||||||
/>;
|
|
||||||
}) }
|
|
||||||
</div>
|
|
||||||
) : null }
|
|
||||||
|
|
||||||
{ dms.length > 0 ? (
|
|
||||||
<div className="mx_AddExistingToSpace_section">
|
|
||||||
<h3>{ _t("Direct Messages") }</h3>
|
|
||||||
{ dms.map(room => {
|
|
||||||
return <Entry
|
|
||||||
key={room.roomId}
|
|
||||||
room={room}
|
|
||||||
checked={selected.has(room)}
|
|
||||||
onChange={onChange ? (checked) => {
|
|
||||||
onChange(checked, room);
|
|
||||||
} : null}
|
|
||||||
/>;
|
|
||||||
}) }
|
|
||||||
</div>
|
|
||||||
) : null }
|
|
||||||
|
|
||||||
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
|
|
||||||
{ _t("No results") }
|
|
||||||
</span> : undefined }
|
|
||||||
</AutoHideScrollbar>
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
|
||||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
|
||||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
|
||||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
|
||||||
|
|
||||||
const [progress, setProgress] = useState<number>(null);
|
|
||||||
const [error, setError] = useState<Error>(null);
|
|
||||||
|
|
||||||
let spaceOptionSection;
|
|
||||||
if (existingSubspaces.length > 0) {
|
|
||||||
const options = [space, ...existingSubspaces].map((space) => {
|
|
||||||
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
|
||||||
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
|
||||||
});
|
|
||||||
return <div key={space.roomId} className={classes}>
|
|
||||||
<RoomAvatar room={space} width={24} height={24} />
|
|
||||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
|
||||||
</div>;
|
|
||||||
});
|
|
||||||
|
|
||||||
spaceOptionSection = (
|
|
||||||
<Dropdown
|
|
||||||
id="mx_SpaceSelectDropdown"
|
|
||||||
onOptionChange={(key: string) => {
|
|
||||||
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
|
|
||||||
}}
|
|
||||||
value={selectedSpace.roomId}
|
|
||||||
label={_t("Space selection")}
|
|
||||||
>
|
|
||||||
{ options }
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
|
|
||||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = <React.Fragment>
|
|
||||||
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
|
||||||
<div>
|
|
||||||
<h1>{ _t("Add existing rooms") }</h1>
|
|
||||||
{ spaceOptionSection }
|
|
||||||
</div>
|
|
||||||
</React.Fragment>;
|
|
||||||
|
|
||||||
const addRooms = async () => {
|
const addRooms = async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
|
@ -269,20 +168,145 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||||
</div>
|
</div>
|
||||||
</span>;
|
</span>;
|
||||||
} else {
|
} else {
|
||||||
|
let button = emptySelectionButton;
|
||||||
|
if (!button || selectedToAdd.size > 0) {
|
||||||
|
button = <AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
|
||||||
|
{ _t("Add") }
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
footer = <>
|
footer = <>
|
||||||
<span>
|
<span>
|
||||||
<div>{ _t("Want to add a new room instead?") }</div>
|
{ footerPrompt }
|
||||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
|
||||||
{ _t("Create a new room") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
|
{ button }
|
||||||
{ _t("Add") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onChange = !busy && !error ? (checked, room) => {
|
||||||
|
if (checked) {
|
||||||
|
selectedToAdd.add(room);
|
||||||
|
} else {
|
||||||
|
selectedToAdd.delete(room);
|
||||||
|
}
|
||||||
|
setSelectedToAdd(new Set(selectedToAdd));
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
return <div className="mx_AddExistingToSpace">
|
||||||
|
<SearchBox
|
||||||
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
|
placeholder={ _t("Filter your rooms and spaces") }
|
||||||
|
onSearch={setQuery}
|
||||||
|
autoComplete={true}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
||||||
|
{ rooms.length > 0 ? (
|
||||||
|
<div className="mx_AddExistingToSpace_section">
|
||||||
|
<h3>{ _t("Rooms") }</h3>
|
||||||
|
{ rooms.map(room => {
|
||||||
|
return <Entry
|
||||||
|
key={room.roomId}
|
||||||
|
room={room}
|
||||||
|
checked={selectedToAdd.has(room)}
|
||||||
|
onChange={onChange ? (checked) => {
|
||||||
|
onChange(checked, room);
|
||||||
|
} : null}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : undefined }
|
||||||
|
|
||||||
|
{ spaces.length > 0 ? (
|
||||||
|
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
|
||||||
|
<h3>{ _t("Spaces") }</h3>
|
||||||
|
<div className="mx_AddExistingToSpace_section_experimental">
|
||||||
|
<div>{ _t("Feeling experimental?") }</div>
|
||||||
|
<div>{ _t("You can add existing spaces to a space.") }</div>
|
||||||
|
</div>
|
||||||
|
{ spaces.map(space => {
|
||||||
|
return <Entry
|
||||||
|
key={space.roomId}
|
||||||
|
room={space}
|
||||||
|
checked={selectedToAdd.has(space)}
|
||||||
|
onChange={onChange ? (checked) => {
|
||||||
|
onChange(checked, space);
|
||||||
|
} : null}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
{ dms.length > 0 ? (
|
||||||
|
<div className="mx_AddExistingToSpace_section">
|
||||||
|
<h3>{ _t("Direct Messages") }</h3>
|
||||||
|
{ dms.map(room => {
|
||||||
|
return <Entry
|
||||||
|
key={room.roomId}
|
||||||
|
room={room}
|
||||||
|
checked={selectedToAdd.has(room)}
|
||||||
|
onChange={onChange ? (checked) => {
|
||||||
|
onChange(checked, room);
|
||||||
|
} : null}
|
||||||
|
/>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
|
||||||
|
{ _t("No results") }
|
||||||
|
</span> : undefined }
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
|
||||||
|
<div className="mx_AddExistingToSpace_footer">
|
||||||
|
{ footer }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
||||||
|
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||||
|
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||||
|
|
||||||
|
let spaceOptionSection;
|
||||||
|
if (existingSubspaces.length > 0) {
|
||||||
|
const options = [space, ...existingSubspaces].map((space) => {
|
||||||
|
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
||||||
|
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
||||||
|
});
|
||||||
|
return <div key={space.roomId} className={classes}>
|
||||||
|
<RoomAvatar room={space} width={24} height={24} />
|
||||||
|
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
spaceOptionSection = (
|
||||||
|
<Dropdown
|
||||||
|
id="mx_SpaceSelectDropdown"
|
||||||
|
onOptionChange={(key: string) => {
|
||||||
|
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
|
||||||
|
}}
|
||||||
|
value={selectedSpace.roomId}
|
||||||
|
label={_t("Space selection")}
|
||||||
|
>
|
||||||
|
{ options }
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
|
||||||
|
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = <React.Fragment>
|
||||||
|
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
||||||
|
<div>
|
||||||
|
<h1>{ _t("Add existing rooms") }</h1>
|
||||||
|
{ spaceOptionSection }
|
||||||
|
</div>
|
||||||
|
</React.Fragment>;
|
||||||
|
|
||||||
return <BaseDialog
|
return <BaseDialog
|
||||||
title={title}
|
title={title}
|
||||||
className="mx_AddExistingToSpaceDialog"
|
className="mx_AddExistingToSpaceDialog"
|
||||||
|
@ -293,21 +317,16 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||||
<MatrixClientContext.Provider value={cli}>
|
<MatrixClientContext.Provider value={cli}>
|
||||||
<AddExistingToSpace
|
<AddExistingToSpace
|
||||||
space={space}
|
space={space}
|
||||||
selected={selectedToAdd}
|
onFinished={onFinished}
|
||||||
onChange={!busy && !error ? (checked, room) => {
|
footerPrompt={<>
|
||||||
if (checked) {
|
<div>{ _t("Want to add a new room instead?") }</div>
|
||||||
selectedToAdd.add(room);
|
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||||
} else {
|
{ _t("Create a new room") }
|
||||||
selectedToAdd.delete(room);
|
</AccessibleButton>
|
||||||
}
|
</>}
|
||||||
setSelectedToAdd(new Set(selectedToAdd));
|
|
||||||
} : null}
|
|
||||||
/>
|
/>
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
|
|
||||||
<div className="mx_AddExistingToSpaceDialog_footer">
|
|
||||||
{ footer }
|
|
||||||
</div>
|
|
||||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||||
</BaseDialog>;
|
</BaseDialog>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import BaseDialog from './BaseDialog';
|
import BaseDialog from './BaseDialog';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t, getUserLanguage } from '../../../languageHandler';
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import {
|
import {
|
||||||
ClientWidgetApi,
|
ClientWidgetApi,
|
||||||
|
@ -39,6 +39,8 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||||
import { arrayFastClone } from "../../../utils/arrays";
|
import { arrayFastClone } from "../../../utils/arrays";
|
||||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {ELEMENT_CLIENT_ID} from "../../../identifiers";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
widgetDefinition: IModalWidgetOpenRequestData;
|
widgetDefinition: IModalWidgetOpenRequestData;
|
||||||
|
@ -129,6 +131,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||||
currentUserId: MatrixClientPeg.get().getUserId(),
|
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||||
userDisplayName: OwnProfileStore.instance.displayName,
|
userDisplayName: OwnProfileStore.instance.displayName,
|
||||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||||
|
clientId: ELEMENT_CLIENT_ID,
|
||||||
|
clientTheme: SettingsStore.getValue("theme"),
|
||||||
|
clientLanguage: getUserLanguage(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = new URL(templated);
|
const parsed = new URL(templated);
|
||||||
|
|
|
@ -217,6 +217,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
||||||
value={this.state.otherHomeserver}
|
value={this.state.otherHomeserver}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
validateOnFocus={false}
|
validateOnFocus={false}
|
||||||
|
id="mx_homeserverInput"
|
||||||
/>
|
/>
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -345,6 +345,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
||||||
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
|
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
|
id="mx_passPhraseInput"
|
||||||
className="mx_AccessSecretStorageDialog_passPhraseInput"
|
className="mx_AccessSecretStorageDialog_passPhraseInput"
|
||||||
onChange={this.onPassPhraseChange}
|
onChange={this.onPassPhraseChange}
|
||||||
value={this.state.passPhrase}
|
value={this.state.passPhrase}
|
||||||
|
|
|
@ -37,7 +37,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
||||||
effect = new Effect(options);
|
effect = new Effect(options);
|
||||||
effectsRef.current[name] = effect;
|
effectsRef.current[name] = effect;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err);
|
console.warn(`Unable to load effect module at '../../../effects/${name}.`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return effect;
|
return effect;
|
||||||
|
|
|
@ -207,6 +207,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
a.href = this.props.src;
|
a.href = this.props.src;
|
||||||
a.download = this.props.name;
|
a.download = this.props.name;
|
||||||
a.target = "_blank";
|
a.target = "_blank";
|
||||||
|
a.rel = "noreferrer noopener";
|
||||||
a.click();
|
a.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -58,13 +58,8 @@ export default class LanguageDropdown extends React.Component {
|
||||||
// If no value is given, we start with the first
|
// If no value is given, we start with the first
|
||||||
// country selected, but our parent component
|
// country selected, but our parent component
|
||||||
// doesn't know this, therefore we do this.
|
// doesn't know this, therefore we do this.
|
||||||
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
const language = languageHandler.getUserLanguage();
|
||||||
if (language) {
|
|
||||||
this.props.onOptionChange(language);
|
this.props.onOptionChange(language);
|
||||||
} else {
|
|
||||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
|
||||||
this.props.onOptionChange(language);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessi
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import {canCancel} from "../context_menus/MessageContextMenu";
|
import {canCancel} from "../context_menus/MessageContextMenu";
|
||||||
import Resend from "../../../Resend";
|
import Resend from "../../../Resend";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
||||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||||
|
@ -122,6 +123,10 @@ export default class MessageActionBar extends React.PureComponent {
|
||||||
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
||||||
this.props.mxEvent.on("Event.status", this.onSent);
|
this.props.mxEvent.on("Event.status", this.onSent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
client.decryptEventIfNeeded(this.props.mxEvent);
|
||||||
|
|
||||||
if (this.props.mxEvent.isBeingDecrypted()) {
|
if (this.props.mxEvent.isBeingDecrypted()) {
|
||||||
this.props.mxEvent.once("Event.decrypted", this.onDecrypted);
|
this.props.mxEvent.once("Event.decrypted", this.onDecrypted);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,10 @@ const ReactButton = ({ mxEvent, reactions }: IProps) => {
|
||||||
})}
|
})}
|
||||||
title={_t("Add reaction")}
|
title={_t("Add reaction")}
|
||||||
onClick={openMenu}
|
onClick={openMenu}
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
openMenu();
|
||||||
|
}}
|
||||||
isExpanded={menuDisplayed}
|
isExpanded={menuDisplayed}
|
||||||
inputRef={button}
|
inputRef={button}
|
||||||
/>
|
/>
|
||||||
|
@ -174,6 +178,8 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
}).filter(item => !!item);
|
}).filter(item => !!item);
|
||||||
|
|
||||||
|
if (!items.length) return null;
|
||||||
|
|
||||||
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
|
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
|
||||||
// The "+ 1" ensure that the "show all" reveals something that takes up
|
// The "+ 1" ensure that the "show all" reveals something that takes up
|
||||||
// more space than the button itself.
|
// more space than the button itself.
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.ViewSourceEvent")
|
@replaceableComponent("views.messages.ViewSourceEvent")
|
||||||
export default class ViewSourceEvent extends React.PureComponent {
|
export default class ViewSourceEvent extends React.PureComponent {
|
||||||
|
@ -36,6 +37,10 @@ export default class ViewSourceEvent extends React.PureComponent {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {mxEvent} = this.props;
|
const {mxEvent} = this.props;
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
client.decryptEventIfNeeded(mxEvent);
|
||||||
|
|
||||||
if (mxEvent.isBeingDecrypted()) {
|
if (mxEvent.isBeingDecrypted()) {
|
||||||
mxEvent.once("Event.decrypted", () => this.forceUpdate());
|
mxEvent.once("Event.decrypted", () => this.forceUpdate());
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,6 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import Analytics from "../../../Analytics";
|
import Analytics from "../../../Analytics";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import { CSSTransition } from "react-transition-group";
|
import { CSSTransition } from "react-transition-group";
|
||||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
|
||||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
|
||||||
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||||
import Toolbar from "../../../accessibility/Toolbar";
|
import Toolbar from "../../../accessibility/Toolbar";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -84,8 +82,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
||||||
|
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
|
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
|
||||||
const roomTags = RoomListStore.instance.getTagsForRoom(r);
|
|
||||||
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
|
|
||||||
return (
|
return (
|
||||||
<RovingAccessibleTooltipButton
|
<RovingAccessibleTooltipButton
|
||||||
className="mx_RoomBreadcrumbs_crumb"
|
className="mx_RoomBreadcrumbs_crumb"
|
||||||
|
@ -98,7 +94,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
||||||
<DecoratedRoomAvatar
|
<DecoratedRoomAvatar
|
||||||
room={r}
|
room={r}
|
||||||
avatarSize={32}
|
avatarSize={32}
|
||||||
tag={roomTag}
|
|
||||||
displayBadge={true}
|
displayBadge={true}
|
||||||
forceCount={true}
|
forceCount={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import RoomTopic from "../elements/RoomTopic";
|
import RoomTopic from "../elements/RoomTopic";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
|
@ -177,7 +176,6 @@ export default class RoomHeader extends React.Component {
|
||||||
roomAvatar = <DecoratedRoomAvatar
|
roomAvatar = <DecoratedRoomAvatar
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
avatarSize={32}
|
avatarSize={32}
|
||||||
tag={DefaultTagID.Untagged} // to apply room publicity badging
|
|
||||||
oobData={this.props.oobData}
|
oobData={this.props.oobData}
|
||||||
viewAvatarOnClick={true}
|
viewAvatarOnClick={true}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -576,7 +576,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
const roomAvatar = <DecoratedRoomAvatar
|
const roomAvatar = <DecoratedRoomAvatar
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
avatarSize={32}
|
avatarSize={32}
|
||||||
tag={this.props.tag}
|
|
||||||
displayBadge={this.props.isMinimized}
|
displayBadge={this.props.isMinimized}
|
||||||
oobData={({avatarUrl: roomProfile.avatarMxc})}
|
oobData={({avatarUrl: roomProfile.avatarMxc})}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {copyPlaintext} from "../../../utils/strings";
|
||||||
import {sleep} from "../../../utils/promise";
|
import {sleep} from "../../../utils/promise";
|
||||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||||
import {showRoomInviteDialog} from "../../../RoomInvite";
|
import {showRoomInviteDialog} from "../../../RoomInvite";
|
||||||
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -50,7 +51,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
|
||||||
<h3>{ _t("Share invite link") }</h3>
|
<h3>{ _t("Share invite link") }</h3>
|
||||||
<span>{ copiedText }</span>
|
<span>{ copiedText }</span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
{ space.canInvite(MatrixClientPeg.get()?.getUserId()) ? <AccessibleButton
|
||||||
className="mx_SpacePublicShare_inviteButton"
|
className="mx_SpacePublicShare_inviteButton"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showRoomInviteDialog(space.roomId);
|
showRoomInviteDialog(space.roomId);
|
||||||
|
@ -59,7 +60,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
|
||||||
>
|
>
|
||||||
<h3>{ _t("Invite people") }</h3>
|
<h3>{ _t("Invite people") }</h3>
|
||||||
<span>{ _t("Invite with email or username") }</span>
|
<span>{ _t("Invite with email or username") }</span>
|
||||||
</AccessibleButton>
|
</AccessibleButton> : null }
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -209,7 +209,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
const userId = this.context.getUserId();
|
const userId = this.context.getUserId();
|
||||||
|
|
||||||
let inviteOption;
|
let inviteOption;
|
||||||
if (this.props.space.canInvite(userId)) {
|
if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) {
|
||||||
inviteOption = (
|
inviteOption = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
className="mx_SpacePanel_contextMenu_inviteButton"
|
className="mx_SpacePanel_contextMenu_inviteButton"
|
||||||
|
|
|
@ -57,8 +57,8 @@ export default class PlaybackWaveform extends React.PureComponent<IProps, IState
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTimeUpdate = (time: number[]) => {
|
private onTimeUpdate = (time: number[]) => {
|
||||||
// Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar.
|
// Track percentages to a general precision to avoid over-waking the component.
|
||||||
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1));
|
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(3));
|
||||||
this.setState({progress});
|
this.setState({progress});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Nurjin Jafar
|
||||||
|
Copyright 2020 Nordeck IT + Consulting GmbH.
|
||||||
|
|
||||||
|
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 type Effect<TOptions extends { [key: string]: any }> = {
|
||||||
|
/**
|
||||||
|
* one or more emojis that will trigger this effect
|
||||||
|
*/
|
||||||
|
emojis: Array<string>;
|
||||||
|
/**
|
||||||
|
* the matrix message type that will trigger this effect
|
||||||
|
*/
|
||||||
|
msgType: string;
|
||||||
|
/**
|
||||||
|
* the room command to trigger this effect
|
||||||
|
*/
|
||||||
|
command: string;
|
||||||
|
/**
|
||||||
|
* a function that returns the translated description of the effect
|
||||||
|
*/
|
||||||
|
description: () => string;
|
||||||
|
/**
|
||||||
|
* a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
|
||||||
|
*/
|
||||||
|
fallbackMessage: () => string;
|
||||||
|
/**
|
||||||
|
* animation options
|
||||||
|
*/
|
||||||
|
options: TOptions;
|
||||||
|
}
|
|
@ -15,80 +15,11 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { _t, _td } from "../languageHandler";
|
import { _t, _td } from "../languageHandler";
|
||||||
|
import { ConfettiOptions } from "./confetti";
|
||||||
export type Effect<TOptions extends { [key: string]: any }> = {
|
import { Effect } from "./effect";
|
||||||
/**
|
import { FireworksOptions } from "./fireworks";
|
||||||
* one or more emojis that will trigger this effect
|
import { SnowfallOptions } from "./snowfall";
|
||||||
*/
|
import { SpaceInvadersOptions } from "./spaceinvaders";
|
||||||
emojis: Array<string>;
|
|
||||||
/**
|
|
||||||
* the matrix message type that will trigger this effect
|
|
||||||
*/
|
|
||||||
msgType: string;
|
|
||||||
/**
|
|
||||||
* the room command to trigger this effect
|
|
||||||
*/
|
|
||||||
command: string;
|
|
||||||
/**
|
|
||||||
* a function that returns the translated description of the effect
|
|
||||||
*/
|
|
||||||
description: () => string;
|
|
||||||
/**
|
|
||||||
* a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
|
|
||||||
*/
|
|
||||||
fallbackMessage: () => string;
|
|
||||||
/**
|
|
||||||
* animation options
|
|
||||||
*/
|
|
||||||
options: TOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfettiOptions = {
|
|
||||||
/**
|
|
||||||
* max confetti count
|
|
||||||
*/
|
|
||||||
maxCount: number;
|
|
||||||
/**
|
|
||||||
* particle animation speed
|
|
||||||
*/
|
|
||||||
speed: number;
|
|
||||||
/**
|
|
||||||
* the confetti animation frame interval in milliseconds
|
|
||||||
*/
|
|
||||||
frameInterval: number;
|
|
||||||
/**
|
|
||||||
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
|
|
||||||
*/
|
|
||||||
alpha: number;
|
|
||||||
/**
|
|
||||||
* use gradient instead of solid particle color
|
|
||||||
*/
|
|
||||||
gradient: boolean;
|
|
||||||
};
|
|
||||||
type FireworksOptions = {
|
|
||||||
/**
|
|
||||||
* max fireworks count
|
|
||||||
*/
|
|
||||||
maxCount: number;
|
|
||||||
/**
|
|
||||||
* gravity value that firework adds to shift from it's start position
|
|
||||||
*/
|
|
||||||
gravity: number;
|
|
||||||
}
|
|
||||||
type SnowfallOptions = {
|
|
||||||
/**
|
|
||||||
* The maximum number of snowflakes to render at a given time
|
|
||||||
*/
|
|
||||||
maxCount: number;
|
|
||||||
/**
|
|
||||||
* The amount of gravity to apply to the snowflakes
|
|
||||||
*/
|
|
||||||
gravity: number;
|
|
||||||
/**
|
|
||||||
* The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies.
|
|
||||||
*/
|
|
||||||
maxDrift: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This configuration defines room effects that can be triggered by custom message types and emojis
|
* This configuration defines room effects that can be triggered by custom message types and emojis
|
||||||
|
@ -131,6 +62,17 @@ export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
|
||||||
maxDrift: 5,
|
maxDrift: 5,
|
||||||
},
|
},
|
||||||
} as Effect<SnowfallOptions>,
|
} as Effect<SnowfallOptions>,
|
||||||
|
{
|
||||||
|
emojis: ["👾", "🌌"],
|
||||||
|
msgType: "io.element.effects.space_invaders",
|
||||||
|
command: "spaceinvaders",
|
||||||
|
description: () => _td("Sends the given message with a space themed effect"),
|
||||||
|
fallbackMessage: () => _t("sends space invaders") + " 👾",
|
||||||
|
options: {
|
||||||
|
maxCount: 50,
|
||||||
|
gravity: 0.01,
|
||||||
|
},
|
||||||
|
} as Effect<SpaceInvadersOptions>,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import ICanvasEffect from '../ICanvasEffect';
|
||||||
|
import { arrayFastClone } from "../../utils/arrays";
|
||||||
|
|
||||||
|
export type SpaceInvadersOptions = {
|
||||||
|
/**
|
||||||
|
* The maximum number of invaders to render at a given time
|
||||||
|
*/
|
||||||
|
maxCount: number;
|
||||||
|
/**
|
||||||
|
* The amount of gravity to apply to the invaders
|
||||||
|
*/
|
||||||
|
gravity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Invader = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
xCol: number;
|
||||||
|
gravity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultOptions: SpaceInvadersOptions = {
|
||||||
|
maxCount: 50,
|
||||||
|
gravity: 0.005,
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEY_FRAME_INTERVAL = 15; // 15ms, roughly
|
||||||
|
const GLYPH = "👾";
|
||||||
|
|
||||||
|
export default class SpaceInvaders implements ICanvasEffect {
|
||||||
|
private readonly options: SpaceInvadersOptions;
|
||||||
|
|
||||||
|
constructor(options: { [key: string]: any }) {
|
||||||
|
this.options = {...DefaultOptions, ...options};
|
||||||
|
}
|
||||||
|
|
||||||
|
private context: CanvasRenderingContext2D | null = null;
|
||||||
|
private particles: Array<Invader> = [];
|
||||||
|
private lastAnimationTime: number;
|
||||||
|
|
||||||
|
public isRunning: boolean;
|
||||||
|
|
||||||
|
public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.context = canvas.getContext('2d');
|
||||||
|
this.particles = [];
|
||||||
|
const count = this.options.maxCount;
|
||||||
|
while (this.particles.length < count) {
|
||||||
|
this.particles.push(this.resetParticle({} as Invader, canvas.width, canvas.height));
|
||||||
|
}
|
||||||
|
this.isRunning = true;
|
||||||
|
requestAnimationFrame(this.renderLoop);
|
||||||
|
if (timeout) {
|
||||||
|
window.setTimeout(this.stop, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop = async () => {
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetParticle = (particle: Invader, width: number, height: number): Invader => {
|
||||||
|
particle.x = Math.random() * width;
|
||||||
|
particle.y = Math.random() * -height;
|
||||||
|
particle.xCol = particle.x;
|
||||||
|
particle.gravity = this.options.gravity + (Math.random() * 6) + 4;
|
||||||
|
return particle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLoop = (): void => {
|
||||||
|
if (!this.context || !this.context.canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.particles.length === 0) {
|
||||||
|
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||||
|
} else {
|
||||||
|
const timeDelta = Date.now() - this.lastAnimationTime;
|
||||||
|
if (timeDelta >= KEY_FRAME_INTERVAL || !this.lastAnimationTime) {
|
||||||
|
// Clear the screen first
|
||||||
|
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||||
|
|
||||||
|
this.lastAnimationTime = Date.now();
|
||||||
|
this.animateAndRenderInvaders();
|
||||||
|
}
|
||||||
|
requestAnimationFrame(this.renderLoop);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private animateAndRenderInvaders() {
|
||||||
|
if (!this.context || !this.context.canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.context.font = "50px Twemoji";
|
||||||
|
for (const particle of arrayFastClone(this.particles)) {
|
||||||
|
particle.y += particle.gravity;
|
||||||
|
|
||||||
|
this.context.save();
|
||||||
|
this.context.fillText(GLYPH, particle.x, particle.y);
|
||||||
|
this.context.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -600,6 +600,10 @@
|
||||||
"See when the avatar changes in this room": "See when the avatar changes in this room",
|
"See when the avatar changes in this room": "See when the avatar changes in this room",
|
||||||
"Change the avatar of your active room": "Change the avatar of your active room",
|
"Change the avatar of your active room": "Change the avatar of your active room",
|
||||||
"See when the avatar changes in your active room": "See when the avatar changes in your active room",
|
"See when the avatar changes in your active room": "See when the avatar changes in your active room",
|
||||||
|
"Kick, ban, or invite people to this room, and make you leave": "Kick, ban, or invite people to this room, and make you leave",
|
||||||
|
"See when people join, leave, or are invited to this room": "See when people join, leave, or are invited to this room",
|
||||||
|
"Kick, ban, or invite people to your active room, and make you leave": "Kick, ban, or invite people to your active room, and make you leave",
|
||||||
|
"See when people join, leave, or are invited to your active room": "See when people join, leave, or are invited to your active room",
|
||||||
"Send stickers to this room as you": "Send stickers to this room as you",
|
"Send stickers to this room as you": "Send stickers to this room as you",
|
||||||
"See when a sticker is posted in this room": "See when a sticker is posted in this room",
|
"See when a sticker is posted in this room": "See when a sticker is posted in this room",
|
||||||
"Send stickers to your active room as you": "Send stickers to your active room as you",
|
"Send stickers to your active room as you": "Send stickers to your active room as you",
|
||||||
|
@ -880,6 +884,8 @@
|
||||||
"sends fireworks": "sends fireworks",
|
"sends fireworks": "sends fireworks",
|
||||||
"Sends the given message with snowfall": "Sends the given message with snowfall",
|
"Sends the given message with snowfall": "Sends the given message with snowfall",
|
||||||
"sends snowfall": "sends snowfall",
|
"sends snowfall": "sends snowfall",
|
||||||
|
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
|
||||||
|
"sends space invaders": "sends space invaders",
|
||||||
"unknown person": "unknown person",
|
"unknown person": "unknown person",
|
||||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
||||||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||||
|
@ -2033,15 +2039,15 @@
|
||||||
"Add a new server...": "Add a new server...",
|
"Add a new server...": "Add a new server...",
|
||||||
"%(networkName)s rooms": "%(networkName)s rooms",
|
"%(networkName)s rooms": "%(networkName)s rooms",
|
||||||
"Matrix rooms": "Matrix rooms",
|
"Matrix rooms": "Matrix rooms",
|
||||||
|
"Not all selected were added": "Not all selected were added",
|
||||||
|
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
|
||||||
|
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
|
||||||
"Filter your rooms and spaces": "Filter your rooms and spaces",
|
"Filter your rooms and spaces": "Filter your rooms and spaces",
|
||||||
"Feeling experimental?": "Feeling experimental?",
|
"Feeling experimental?": "Feeling experimental?",
|
||||||
"You can add existing spaces to a space.": "You can add existing spaces to a space.",
|
"You can add existing spaces to a space.": "You can add existing spaces to a space.",
|
||||||
"Direct Messages": "Direct Messages",
|
"Direct Messages": "Direct Messages",
|
||||||
"Space selection": "Space selection",
|
"Space selection": "Space selection",
|
||||||
"Add existing rooms": "Add existing rooms",
|
"Add existing rooms": "Add existing rooms",
|
||||||
"Not all selected were added": "Not all selected were added",
|
|
||||||
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
|
|
||||||
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
|
|
||||||
"Want to add a new room instead?": "Want to add a new room instead?",
|
"Want to add a new room instead?": "Want to add a new room instead?",
|
||||||
"Create a new room": "Create a new room",
|
"Create a new room": "Create a new room",
|
||||||
"Matrix ID": "Matrix ID",
|
"Matrix ID": "Matrix ID",
|
||||||
|
@ -2704,13 +2710,12 @@
|
||||||
"Failed to create initial space rooms": "Failed to create initial space rooms",
|
"Failed to create initial space rooms": "Failed to create initial space rooms",
|
||||||
"Skip for now": "Skip for now",
|
"Skip for now": "Skip for now",
|
||||||
"Creating rooms...": "Creating rooms...",
|
"Creating rooms...": "Creating rooms...",
|
||||||
"Failed to add rooms to space": "Failed to add rooms to space",
|
|
||||||
"Adding...": "Adding...",
|
|
||||||
"What do you want to organise?": "What do you want to organise?",
|
"What do you want to organise?": "What do you want to organise?",
|
||||||
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
|
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
|
||||||
"Share %(name)s": "Share %(name)s",
|
"Share %(name)s": "Share %(name)s",
|
||||||
"It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
|
"It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.",
|
||||||
"Go to my first room": "Go to my first room",
|
"Go to my first room": "Go to my first room",
|
||||||
|
"Go to my space": "Go to my space",
|
||||||
"Who are you working with?": "Who are you working with?",
|
"Who are you working with?": "Who are you working with?",
|
||||||
"Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s",
|
"Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s",
|
||||||
"Just me": "Just me",
|
"Just me": "Just me",
|
||||||
|
@ -2834,6 +2839,7 @@
|
||||||
"Room Notification": "Room Notification",
|
"Room Notification": "Room Notification",
|
||||||
"Notification Autocomplete": "Notification Autocomplete",
|
"Notification Autocomplete": "Notification Autocomplete",
|
||||||
"Room Autocomplete": "Room Autocomplete",
|
"Room Autocomplete": "Room Autocomplete",
|
||||||
|
"Space Autocomplete": "Space Autocomplete",
|
||||||
"Users": "Users",
|
"Users": "Users",
|
||||||
"User Autocomplete": "User Autocomplete",
|
"User Autocomplete": "User Autocomplete",
|
||||||
"We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.",
|
"We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.",
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ELEMENT_CLIENT_ID = "io.element.web";
|
|
@ -177,8 +177,10 @@ export default class EventIndex extends EventEmitter {
|
||||||
* listener.
|
* listener.
|
||||||
*/
|
*/
|
||||||
onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => {
|
onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
// We only index encrypted rooms locally.
|
// We only index encrypted rooms locally.
|
||||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
|
if (!client.isRoomEncrypted(room.roomId)) return;
|
||||||
|
|
||||||
// If it isn't a live event or if it's redacted there's nothing to
|
// If it isn't a live event or if it's redacted there's nothing to
|
||||||
// do.
|
// do.
|
||||||
|
@ -187,10 +189,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.isBeingDecrypted()) {
|
await client.decryptEventIfNeeded(ev);
|
||||||
// XXX: Private member access
|
|
||||||
await ev._decryptionPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.addLiveEventToIndex(ev);
|
await this.addLiveEventToIndex(ev);
|
||||||
}
|
}
|
||||||
|
@ -518,19 +517,10 @@ export default class EventIndex extends EventEmitter {
|
||||||
const decryptionPromises = matrixEvents
|
const decryptionPromises = matrixEvents
|
||||||
.filter(event => event.isEncrypted())
|
.filter(event => event.isEncrypted())
|
||||||
.map(event => {
|
.map(event => {
|
||||||
if (event.shouldAttemptDecryption()) {
|
return client.decryptEventIfNeeded(event, {
|
||||||
return event.attemptDecryption(client._crypto, {
|
|
||||||
isRetry: true,
|
isRetry: true,
|
||||||
emit: false,
|
emit: false,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// TODO the decryption promise is a private property, this
|
|
||||||
// should either be made public or we should convert the
|
|
||||||
// event that gets fired when decryption is done into a
|
|
||||||
// promise using the once event emitter method:
|
|
||||||
// https://nodejs.org/api/events.html#events_events_once_emitter_name
|
|
||||||
return event._decryptionPromise;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Let us wait for all the events to get decrypted.
|
// Let us wait for all the events to get decrypted.
|
||||||
|
|
|
@ -56,6 +56,15 @@ export function newTranslatableError(message: string) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUserLanguage(): string {
|
||||||
|
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||||
|
if (language) {
|
||||||
|
return language;
|
||||||
|
} else {
|
||||||
|
return normalizeLanguageKey(getLanguageFromBrowser());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function which only purpose is to mark that a string is translatable
|
// Function which only purpose is to mark that a string is translatable
|
||||||
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
|
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
|
||||||
export function _td(s: string): string {
|
export function _td(s: string): string {
|
||||||
|
@ -455,10 +464,14 @@ function getLangsJson(): Promise<object> {
|
||||||
request(
|
request(
|
||||||
{ method: "GET", url },
|
{ method: "GET", url },
|
||||||
(err, response, body) => {
|
(err, response, body) => {
|
||||||
if (err || response.status < 200 || response.status >= 300) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
reject(new Error(`Failed to load ${url}, got ${response.status}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
resolve(JSON.parse(body));
|
resolve(JSON.parse(body));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -498,10 +511,14 @@ function getLanguage(langPath: string): Promise<object> {
|
||||||
request(
|
request(
|
||||||
{ method: "GET", url: langPath },
|
{ method: "GET", url: langPath },
|
||||||
(err, response, body) => {
|
(err, response, body) => {
|
||||||
if (err || response.status < 200 || response.status >= 300) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
reject(new Error(`Failed to load ${langPath}, got ${response.status}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
resolve(weblateToCounterpart(JSON.parse(body)));
|
resolve(weblateToCounterpart(JSON.parse(body)));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -62,7 +62,7 @@ export const getOrder = (order: string, creationTs: number, roomId: string): Arr
|
||||||
|
|
||||||
if (typeof order === "string" && Array.from(order).every((c: string) => {
|
if (typeof order === "string" && Array.from(order).every((c: string) => {
|
||||||
const charCode = c.charCodeAt(0);
|
const charCode = c.charCodeAt(0);
|
||||||
return charCode >= 0x20 && charCode <= 0x7F;
|
return charCode >= 0x20 && charCode <= 0x7E;
|
||||||
})) {
|
})) {
|
||||||
validatedOrder = order;
|
validatedOrder = order;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
||||||
import { getListAlgorithmInstance } from "./list-ordering";
|
import { getListAlgorithmInstance } from "./list-ordering";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
||||||
|
import { MultiLock } from "../../../utils/MultiLock";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired when the Algorithm has determined a list has been updated.
|
* Fired when the Algorithm has determined a list has been updated.
|
||||||
|
@ -77,6 +78,7 @@ export class Algorithm extends EventEmitter {
|
||||||
} = {};
|
} = {};
|
||||||
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
||||||
private allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
private allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
||||||
|
private handlerLock = new MultiLock();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set to true to suspend emissions of algorithm updates.
|
* Set to true to suspend emissions of algorithm updates.
|
||||||
|
@ -677,6 +679,12 @@ export class Algorithm extends EventEmitter {
|
||||||
* processing.
|
* processing.
|
||||||
*/
|
*/
|
||||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||||
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
|
console.log(`Acquiring lock for ${room.roomId} with cause ${cause}`);
|
||||||
|
}
|
||||||
|
const release = await this.handlerLock.acquire(room.roomId);
|
||||||
|
try {
|
||||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
|
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
|
||||||
|
@ -712,7 +720,7 @@ export class Algorithm extends EventEmitter {
|
||||||
// If we have tags for a room and don't have the room referenced, something went horribly
|
// If we have tags for a room and don't have the room referenced, something went horribly
|
||||||
// wrong - the reference should have been updated above.
|
// wrong - the reference should have been updated above.
|
||||||
if (hasTags && !knownRoomRef && !isSticky) {
|
if (hasTags && !knownRoomRef && !isSticky) {
|
||||||
throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`);
|
throw new Error(`${room.roomId} is missing from room array but is known`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Like above, update the reference to the sticky room if we need to
|
// Like above, update the reference to the sticky room if we need to
|
||||||
|
@ -800,7 +808,9 @@ export class Algorithm extends EventEmitter {
|
||||||
if (this.stickyRoom === room) {
|
if (this.stickyRoom === room) {
|
||||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
|
console.warn(
|
||||||
|
`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -862,8 +872,13 @@ export class Algorithm extends EventEmitter {
|
||||||
|
|
||||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
|
console.log(
|
||||||
|
`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return changed;
|
return changed;
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
* Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -52,6 +52,8 @@ import {getCustomTheme} from "../../theme";
|
||||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||||
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
|
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { ELEMENT_CLIENT_ID } from "../../identifiers";
|
||||||
|
import { getUserLanguage } from "../../languageHandler";
|
||||||
|
|
||||||
// TODO: Destroy all of this code
|
// TODO: Destroy all of this code
|
||||||
|
|
||||||
|
@ -194,6 +196,9 @@ export class StopGapWidget extends EventEmitter {
|
||||||
currentUserId: MatrixClientPeg.get().getUserId(),
|
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||||
userDisplayName: OwnProfileStore.instance.displayName,
|
userDisplayName: OwnProfileStore.instance.displayName,
|
||||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||||
|
clientId: ELEMENT_CLIENT_ID,
|
||||||
|
clientTheme: SettingsStore.getValue("theme"),
|
||||||
|
clientLanguage: getUserLanguage(),
|
||||||
}, opts?.asPopout);
|
}, opts?.asPopout);
|
||||||
|
|
||||||
const parsed = new URL(templated);
|
const parsed = new URL(templated);
|
||||||
|
|
|
@ -44,6 +44,7 @@ import { CHAT_EFFECTS } from "../../effects";
|
||||||
import { containsEmoji } from "../../effects/utils";
|
import { containsEmoji } from "../../effects/utils";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import {tryTransformPermalinkToLocalHref} from "../../utils/permalinks/Permalinks";
|
import {tryTransformPermalinkToLocalHref} from "../../utils/permalinks/Permalinks";
|
||||||
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
// TODO: Purge this from the universe
|
// TODO: Purge this from the universe
|
||||||
|
|
||||||
|
@ -144,6 +145,52 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
return {roomId, eventId: r.event_id};
|
return {roomId, eventId: r.event_id};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<MatrixEvent[]> {
|
||||||
|
limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const roomId = ActiveRoomObserver.activeRoomId;
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
|
||||||
|
|
||||||
|
const results: MatrixEvent[] = [];
|
||||||
|
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
|
||||||
|
for (let i = events.length - 1; i > 0; i--) {
|
||||||
|
if (results.length >= limit) break;
|
||||||
|
|
||||||
|
const ev = events[i];
|
||||||
|
if (ev.getType() !== eventType) continue;
|
||||||
|
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue;
|
||||||
|
results.push(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.map(e => e.event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async readStateEvents(
|
||||||
|
eventType: string, stateKey: string | undefined, limit: number,
|
||||||
|
): Promise<MatrixEvent[]> {
|
||||||
|
limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const roomId = ActiveRoomObserver.activeRoomId;
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
|
||||||
|
|
||||||
|
const results: MatrixEvent[] = [];
|
||||||
|
const state = room.currentState.events.get(eventType);
|
||||||
|
if (state) {
|
||||||
|
if (stateKey === "" || !!stateKey) {
|
||||||
|
const forKey = state.get(stateKey);
|
||||||
|
if (forKey) results.push(forKey);
|
||||||
|
} else {
|
||||||
|
results.push(...Array.from(state.values()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.slice(0, limit).map(e => e.event);
|
||||||
|
}
|
||||||
|
|
||||||
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
|
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
|
||||||
const oidcState = WidgetPermissionStore.instance.getOIDCState(
|
const oidcState = WidgetPermissionStore.instance.getOIDCState(
|
||||||
this.forWidget, this.forWidgetKind, this.inRoomId,
|
this.forWidget, this.forWidgetKind, this.inRoomId,
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EnhancedMap } from "./maps";
|
||||||
|
import AwaitLock from "await-lock";
|
||||||
|
|
||||||
|
export type DoneFn = () => void;
|
||||||
|
|
||||||
|
export class MultiLock {
|
||||||
|
private locks = new EnhancedMap<string, AwaitLock>();
|
||||||
|
|
||||||
|
public async acquire(key: string): Promise<DoneFn> {
|
||||||
|
const lock = this.locks.getOrCreate(key, new AwaitLock());
|
||||||
|
await lock.acquireAsync();
|
||||||
|
return () => lock.release();
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,7 +75,8 @@ export function arraySmoothingResample(input: number[], points: number): number[
|
||||||
for (let i = 1; i < input.length - 1; i += 2) {
|
for (let i = 1; i < input.length - 1; i += 2) {
|
||||||
const prevPoint = input[i - 1];
|
const prevPoint = input[i - 1];
|
||||||
const nextPoint = input[i + 1];
|
const nextPoint = input[i + 1];
|
||||||
const average = (prevPoint + nextPoint) / 2;
|
const currPoint = input[i];
|
||||||
|
const average = (prevPoint + nextPoint + currPoint) / 3;
|
||||||
samples.push(average);
|
samples.push(average);
|
||||||
}
|
}
|
||||||
input = samples;
|
input = samples;
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {SimpleObservable} from "matrix-widget-api";
|
||||||
import { IDestroyable } from "../utils/IDestroyable";
|
import { IDestroyable } from "../utils/IDestroyable";
|
||||||
import { PlaybackClock } from "./PlaybackClock";
|
import { PlaybackClock } from "./PlaybackClock";
|
||||||
import { createAudioContext, decodeOgg } from "./compat";
|
import { createAudioContext, decodeOgg } from "./compat";
|
||||||
|
import { clamp } from "../utils/numbers";
|
||||||
|
|
||||||
export enum PlaybackState {
|
export enum PlaybackState {
|
||||||
Decoding = "decoding",
|
Decoding = "decoding",
|
||||||
|
@ -33,9 +34,20 @@ export const PLAYBACK_WAVEFORM_SAMPLES = 39;
|
||||||
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
|
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
|
||||||
|
|
||||||
function makePlaybackWaveform(input: number[]): number[] {
|
function makePlaybackWaveform(input: number[]): number[] {
|
||||||
// We use a smoothing resample to keep the rough shape of the waveform the user will be seeing. We
|
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
||||||
// then rescale so the user can see the waveform properly (loud noises == 100%).
|
const noiseWaveform = input.map(v => Math.abs(v));
|
||||||
return arrayRescale(arraySmoothingResample(input, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
|
||||||
|
// Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
||||||
|
// We also rescale the waveform to be 0-1 for the remaining function logic.
|
||||||
|
const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
|
||||||
|
|
||||||
|
// Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled
|
||||||
|
// waveform. Most speech happens below the 0.5 mark.
|
||||||
|
const filtered = resampled.map(v => clamp(v, 0.1, 0.5));
|
||||||
|
|
||||||
|
// Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something
|
||||||
|
// sensible. This is what we return to keep our contract of "values between zero and one".
|
||||||
|
return arrayRescale(filtered, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Playback extends EventEmitter implements IDestroyable {
|
export class Playback extends EventEmitter implements IDestroyable {
|
||||||
|
@ -126,6 +138,7 @@ export class Playback extends EventEmitter implements IDestroyable {
|
||||||
this.waveformObservable.update(this.resampledWaveform);
|
this.waveformObservable.update(this.resampledWaveform);
|
||||||
|
|
||||||
this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
|
this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
|
||||||
|
this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update
|
||||||
this.clock.durationSeconds = this.audioBuf.duration;
|
this.clock.durationSeconds = this.audioBuf.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,15 @@ export class PlaybackClock implements IDestroyable {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the time in the audio context where the clip starts/has been loaded.
|
||||||
|
* This is to ensure the clock isn't skewed into thinking it is ~0.5s into
|
||||||
|
* a clip when the duration is set.
|
||||||
|
*/
|
||||||
|
public flagLoadTime() {
|
||||||
|
this.clipStart = this.context.currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
public flagStart() {
|
public flagStart() {
|
||||||
if (this.stopped) {
|
if (this.stopped) {
|
||||||
this.clipStart = this.context.currentTime;
|
this.clipStart = this.context.currentTime;
|
||||||
|
|
|
@ -96,6 +96,16 @@ export class CapabilityText {
|
||||||
[EventDirection.Receive]: _td("See when the avatar changes in your active room"),
|
[EventDirection.Receive]: _td("See when the avatar changes in your active room"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[EventType.RoomMember]: {
|
||||||
|
[WidgetKind.Room]: {
|
||||||
|
[EventDirection.Send]: _td("Kick, ban, or invite people to this room, and make you leave"),
|
||||||
|
[EventDirection.Receive]: _td("See when people join, leave, or are invited to this room"),
|
||||||
|
},
|
||||||
|
[GENERIC_WIDGET_KIND]: {
|
||||||
|
[EventDirection.Send]: _td("Kick, ban, or invite people to your active room, and make you leave"),
|
||||||
|
[EventDirection.Receive]: _td("See when people join, leave, or are invited to your active room"),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
private static nonStateSendRecvCaps: ISendRecvStaticCapText = {
|
private static nonStateSendRecvCaps: ISendRecvStaticCapText = {
|
||||||
|
|
|
@ -95,6 +95,7 @@ export function createTestClient() {
|
||||||
getItem: jest.fn(),
|
getItem: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
decryptEventIfNeeded: () => Promise.resolve(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,10 +73,10 @@ describe('arrays', () => {
|
||||||
// we'd be feeding a thousand values in and seeing what a curve of 250 values looks like,
|
// we'd be feeding a thousand values in and seeing what a curve of 250 values looks like,
|
||||||
// but that's not really feasible to manually verify accuracy.
|
// but that's not really feasible to manually verify accuracy.
|
||||||
[
|
[
|
||||||
{input: [2, 2, 0, 2, 2, 0, 2, 2, 0], output: [1, 1, 2, 1]}, // Odd -> Even
|
{input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3, 3]}, // Odd -> Even
|
||||||
{input: [2, 2, 0, 2, 2, 0, 2, 2, 0], output: [1, 1, 2]}, // Odd -> Odd
|
{input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3]}, // Odd -> Odd
|
||||||
{input: [2, 2, 0, 2, 2, 0, 2, 2], output: [1, 1, 2]}, // Even -> Odd
|
{input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3, 3]}, // Even -> Odd
|
||||||
{input: [2, 2, 0, 2, 2, 0, 2, 2], output: [1, 2]}, // Even -> Even
|
{input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3]}, // Even -> Even
|
||||||
].forEach((c, i) => expectSample(i, c.input, c.output, true));
|
].forEach((c, i) => expectSample(i, c.input, c.output, true));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -5670,8 +5670,8 @@ mathml-tag-names@^2.1.3:
|
||||||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||||
version "10.1.0"
|
version "11.0.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2d73805ca3d8c5a140fe05e574f826696de1656a"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/52a893a8116d60bb76f1b015d3161a15404b3628"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
|
@ -5704,10 +5704,10 @@ matrix-react-test-utils@^0.2.2:
|
||||||
"@babel/traverse" "^7.13.17"
|
"@babel/traverse" "^7.13.17"
|
||||||
walk "^2.3.14"
|
walk "^2.3.14"
|
||||||
|
|
||||||
matrix-widget-api@^0.1.0-beta.13:
|
matrix-widget-api@^0.1.0-beta.14:
|
||||||
version "0.1.0-beta.13"
|
version "0.1.0-beta.14"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.13.tgz#ebddc83eaef39bbb87b621a02a35902e1a29b9ef"
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70"
|
||||||
integrity sha512-DJAvuX2E7gxc/a9rtJPDh17ba9xGIOAoBHcWirNTN3KGodzsrZ+Ns+M/BREFWMwGS5yEBZko5eq7uhXStEbnyQ==
|
integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/events" "^3.0.0"
|
"@types/events" "^3.0.0"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
|
|
Loading…
Reference in New Issue