Merge branch 'develop' into gsouquet/react-17

This commit is contained in:
Germain Souquet 2021-06-09 11:58:08 +01:00
commit 6e0a908c59
41 changed files with 1212 additions and 391 deletions

View file

@ -1,3 +1,109 @@
Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0)
* Upgrade to JS SDK 11.2.0
* [Release] Fix notif panel timestamp padding
[\#6158](https://github.com/matrix-org/matrix-react-sdk/pull/6158)
Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1)
* Upgrade to JS SDK 11.2.0-rc.1
* Translations update from Weblate
[\#6128](https://github.com/matrix-org/matrix-react-sdk/pull/6128)
* Fix all DMs wrongly appearing in room list when `m.direct` is changed
[\#6122](https://github.com/matrix-org/matrix-react-sdk/pull/6122)
* Update way of checking for registration disabled
[\#6123](https://github.com/matrix-org/matrix-react-sdk/pull/6123)
* Fix the ability to remove avatar from a space via settings
[\#6126](https://github.com/matrix-org/matrix-react-sdk/pull/6126)
* Switch to stable endpoint/fields for MSC2858
[\#6125](https://github.com/matrix-org/matrix-react-sdk/pull/6125)
* Clear stored editor state when canceling editing using a shortcut
[\#6117](https://github.com/matrix-org/matrix-react-sdk/pull/6117)
* Respect newlines in space topics
[\#6124](https://github.com/matrix-org/matrix-react-sdk/pull/6124)
* Add url param `defaultUsername` to prefill the login username field
[\#5674](https://github.com/matrix-org/matrix-react-sdk/pull/5674)
* Bump ws from 7.4.2 to 7.4.6
[\#6115](https://github.com/matrix-org/matrix-react-sdk/pull/6115)
* Sticky headers repositioning without layout trashing
[\#6110](https://github.com/matrix-org/matrix-react-sdk/pull/6110)
* Handle user_busy in voip calls
[\#6112](https://github.com/matrix-org/matrix-react-sdk/pull/6112)
* Avoid showing warning modals from the invite dialog after it unmounts
[\#6105](https://github.com/matrix-org/matrix-react-sdk/pull/6105)
* Fix misleading child counts in spaces
[\#6109](https://github.com/matrix-org/matrix-react-sdk/pull/6109)
* Close creation menu when expanding space panel via expand hierarchy
[\#6090](https://github.com/matrix-org/matrix-react-sdk/pull/6090)
* Prevent having duplicates in pending room state
[\#6108](https://github.com/matrix-org/matrix-react-sdk/pull/6108)
* Update reactions row on event decryption
[\#6106](https://github.com/matrix-org/matrix-react-sdk/pull/6106)
* Destroy playback instance on voice message unmount
[\#6101](https://github.com/matrix-org/matrix-react-sdk/pull/6101)
* Fix message preview not up to date
[\#6102](https://github.com/matrix-org/matrix-react-sdk/pull/6102)
* Convert some Flow typed files to TS (round 2)
[\#6076](https://github.com/matrix-org/matrix-react-sdk/pull/6076)
* Remove unused middlePanelResized event listener
[\#6086](https://github.com/matrix-org/matrix-react-sdk/pull/6086)
* Fix accessing currentState on an invalid joinedRoom
[\#6100](https://github.com/matrix-org/matrix-react-sdk/pull/6100)
* Remove Promise allSettled polyfill as js-sdk uses it directly
[\#6097](https://github.com/matrix-org/matrix-react-sdk/pull/6097)
* Prevent DecoratedRoomAvatar to update its state for the same value
[\#6099](https://github.com/matrix-org/matrix-react-sdk/pull/6099)
* Skip generatePreview if event is not part of the live timeline
[\#6098](https://github.com/matrix-org/matrix-react-sdk/pull/6098)
* fix sticky headers when results num get displayed
[\#6095](https://github.com/matrix-org/matrix-react-sdk/pull/6095)
* Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState
[\#6094](https://github.com/matrix-org/matrix-react-sdk/pull/6094)
* Safeguards to prevent layout trashing for window dimensions
[\#6092](https://github.com/matrix-org/matrix-react-sdk/pull/6092)
* Use local room state to render space hierarchy if the room is known
[\#6089](https://github.com/matrix-org/matrix-react-sdk/pull/6089)
* Add spinner in UserMenu to list pending long running actions
[\#6085](https://github.com/matrix-org/matrix-react-sdk/pull/6085)
* Stop overscroll in Firefox Nightly for macOS
[\#6093](https://github.com/matrix-org/matrix-react-sdk/pull/6093)
* Move SettingsStore watchers/monitors over to ES6 maps for performance
[\#6063](https://github.com/matrix-org/matrix-react-sdk/pull/6063)
* Bump libolm version.
[\#6080](https://github.com/matrix-org/matrix-react-sdk/pull/6080)
* Improve styling of the message action bar
[\#6066](https://github.com/matrix-org/matrix-react-sdk/pull/6066)
* Improve explore rooms when no results are found
[\#6070](https://github.com/matrix-org/matrix-react-sdk/pull/6070)
* Remove logo spinner
[\#6078](https://github.com/matrix-org/matrix-react-sdk/pull/6078)
* Fix add reaction prompt showing even when user is not joined to room
[\#6073](https://github.com/matrix-org/matrix-react-sdk/pull/6073)
* Vectorize spinners
[\#5680](https://github.com/matrix-org/matrix-react-sdk/pull/5680)
* Fix handling of via servers for suggested rooms
[\#6077](https://github.com/matrix-org/matrix-react-sdk/pull/6077)
* Upgrade showChatEffects to room-level setting exposure
[\#6075](https://github.com/matrix-org/matrix-react-sdk/pull/6075)
* Delete RoomView dead code
[\#6071](https://github.com/matrix-org/matrix-react-sdk/pull/6071)
* Reduce noise in tests
[\#6074](https://github.com/matrix-org/matrix-react-sdk/pull/6074)
* Fix room name issues in right panel summary card
[\#6069](https://github.com/matrix-org/matrix-react-sdk/pull/6069)
* Cache normalized room name
[\#6072](https://github.com/matrix-org/matrix-react-sdk/pull/6072)
* Update MemberList to reflect changes for invite permission change
[\#6061](https://github.com/matrix-org/matrix-react-sdk/pull/6061)
* Delete RoomView dead code
[\#6065](https://github.com/matrix-org/matrix-react-sdk/pull/6065)
* Show subspace rooms count even if it is 0 for consistency
[\#6067](https://github.com/matrix-org/matrix-react-sdk/pull/6067)
Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24) Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.22.0", "version": "3.23.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {

View file

@ -76,6 +76,7 @@
@import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss";
@import "./views/dialogs/_ForwardDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_HostSignupDialog.scss";
@import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss";

View file

@ -365,6 +365,45 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
.mx_SpaceRoomView_betaWarning {
padding: 12px 12px 12px 54px;
position: relative;
font-size: $font-15px;
line-height: $font-24px;
width: 432px;
border-radius: 8px;
background-color: $info-plinth-bg-color;
color: $secondary-fg-color;
box-sizing: border-box;
> h3 {
font-weight: $font-semi-bold;
font-size: inherit;
line-height: inherit;
margin: 0;
}
> p {
font-size: inherit;
line-height: inherit;
margin: 0;
}
&::before {
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
content: '';
width: 20px;
height: 20px;
position: absolute;
top: 14px;
left: 14px;
background-color: $secondary-fg-color;
}
}
.mx_SpaceRoomView_inviteTeammates { .mx_SpaceRoomView_inviteTeammates {
// XXX remove this when spaces leaves Beta // XXX remove this when spaces leaves Beta
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer { .mx_SpaceRoomView_inviteTeammates_betaDisclaimer {

View file

@ -0,0 +1,159 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ForwardDialog {
width: 520px;
color: $primary-fg-color;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-height: 0;
height: 80vh;
> h3 {
margin: 0 0 6px;
color: $secondary-fg-color;
font-size: $font-12px;
font-weight: $font-semi-bold;
line-height: $font-15px;
}
> .mx_ForwardDialog_preview {
max-height: 30%;
flex-shrink: 0;
overflow: scroll;
div {
pointer-events: none;
}
.mx_EventTile_msgOption {
display: none;
}
// When forwarding messages from encrypted rooms, EventTile will complain
// that our preview is unencrypted, which doesn't actually matter
.mx_EventTile_e2eIcon_unencrypted {
display: none;
}
// We also hide download links to not encourage users to try interacting
.mx_MFileBody_download {
display: none;
}
}
> hr {
width: 100%;
border: none;
border-top: 1px solid $input-border-color;
margin: 12px 0;
}
> .mx_ForwardList {
display: contents;
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
}
.mx_ForwardList_content {
flex-grow: 1;
}
.mx_ForwardList_noResults {
display: block;
margin-top: 24px;
}
.mx_ForwardList_results {
&:not(:first-child) {
margin-top: 24px;
}
.mx_ForwardList_entry {
display: flex;
justify-content: space-between;
height: 32px;
padding: 6px;
border-radius: 8px;
&:hover {
background-color: $groupFilterPanel-bg-color;
}
.mx_ForwardList_roomButton {
display: flex;
margin-right: 12px;
min-width: 0;
.mx_DecoratedRoomAvatar {
margin-right: 12px;
}
.mx_ForwardList_entry_name {
font-size: $font-15px;
line-height: 30px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 12px;
}
}
.mx_ForwardList_sendButton {
position: relative;
&:not(.mx_ForwardList_canSend) .mx_ForwardList_sendLabel {
// Hide the "Send" label while preserving button size
visibility: hidden;
}
.mx_ForwardList_sendIcon, .mx_NotificationBadge {
position: absolute;
}
.mx_NotificationBadge {
// Match the failed to send indicator's color with the disabled button
background-color: $button-danger-disabled-fg-color;
}
&.mx_ForwardList_sending .mx_ForwardList_sendIcon {
background-color: $button-primary-bg-color;
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: 14px;
width: 14px;
height: 14px;
}
&.mx_ForwardList_sent .mx_ForwardList_sendIcon {
background-color: $button-primary-bg-color;
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: 14px;
width: 14px;
height: 14px;
}
}
}
}
}
}

View file

@ -32,4 +32,59 @@ limitations under the License.
margin-right: 6px; margin-right: 6px;
} }
} }
.mx_PinnedMessagesCard_empty {
display: flex;
height: 100%;
> div {
height: max-content;
text-align: center;
margin: auto 40px;
.mx_PinnedMessagesCard_MessageActionBar {
pointer-events: none;
display: flex;
height: 32px;
line-height: $font-24px;
border-radius: 8px;
background: $primary-bg-color;
border: 1px solid $input-border-color;
padding: 1px;
width: max-content;
margin: 0 auto;
box-sizing: border-box;
.mx_MessageActionBar_maskButton {
display: inline-block;
position: relative;
}
.mx_MessageActionBar_optionsButton {
background: $roomlist-button-bg-color;
border-radius: 6px;
z-index: 1;
&::after {
background-color: $primary-fg-color;
}
}
}
> h2 {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
margin-top: 24px;
margin-bottom: 20px;
}
> span {
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
}
}
}
} }

View file

@ -21,153 +21,161 @@ import SettingsStore from "./settings/SettingsStore";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList"; import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore"; import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
function textForMemberEvent(ev) { // These functions are frequently used just to check whether an event has
// any text to display at all. For this reason they return deferred values
// to avoid the expense of looking up translations when they're not needed.
function textForMemberEvent(ev): () => string | null {
// XXX: SYJS-16 "sender is sometimes null for join messages" // XXX: SYJS-16 "sender is sometimes null for join messages"
const senderName = ev.sender ? ev.sender.name : ev.getSender(); const senderName = ev.sender ? ev.sender.name : ev.getSender();
const targetName = ev.target ? ev.target.name : ev.getStateKey(); const targetName = ev.target ? ev.target.name : ev.getStateKey();
const prevContent = ev.getPrevContent(); const prevContent = ev.getPrevContent();
const content = ev.getContent(); const content = ev.getContent();
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : ''; const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
switch (content.membership) { switch (content.membership) {
case 'invite': { case 'invite': {
const threePidContent = content.third_party_invite; const threePidContent = content.third_party_invite;
if (threePidContent) { if (threePidContent) {
if (threePidContent.display_name) { if (threePidContent.display_name) {
return _t('%(targetName)s accepted the invitation for %(displayName)s.', { return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
targetName, targetName,
displayName: threePidContent.display_name, displayName: threePidContent.display_name,
}); });
} else { } else {
return _t('%(targetName)s accepted an invitation.', {targetName}); return () => _t('%(targetName)s accepted an invitation.', {targetName});
} }
} else { } else {
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName}); return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
} }
} }
case 'ban': case 'ban':
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason; return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
case 'join': case 'join':
if (prevContent && prevContent.membership === 'join') { if (prevContent && prevContent.membership === 'join') {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', { return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
oldDisplayName: prevContent.displayname, oldDisplayName: prevContent.displayname,
displayName: content.displayname, displayName: content.displayname,
}); });
} else if (!prevContent.displayname && content.displayname) { } else if (!prevContent.displayname && content.displayname) {
return _t('%(senderName)s set their display name to %(displayName)s.', { return () => _t('%(senderName)s set their display name to %(displayName)s.', {
senderName: ev.getSender(), senderName: ev.getSender(),
displayName: content.displayname, displayName: content.displayname,
}); });
} else if (prevContent.displayname && !content.displayname) { } else if (prevContent.displayname && !content.displayname) {
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', { return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
senderName, senderName,
oldDisplayName: prevContent.displayname, oldDisplayName: prevContent.displayname,
}); });
} else if (prevContent.avatar_url && !content.avatar_url) { } else if (prevContent.avatar_url && !content.avatar_url) {
return _t('%(senderName)s removed their profile picture.', {senderName}); return () => _t('%(senderName)s removed their profile picture.', {senderName});
} else if (prevContent.avatar_url && content.avatar_url && } else if (prevContent.avatar_url && content.avatar_url &&
prevContent.avatar_url !== content.avatar_url) { prevContent.avatar_url !== content.avatar_url) {
return _t('%(senderName)s changed their profile picture.', {senderName}); return () => _t('%(senderName)s changed their profile picture.', {senderName});
} else if (!prevContent.avatar_url && content.avatar_url) { } else if (!prevContent.avatar_url && content.avatar_url) {
return _t('%(senderName)s set a profile picture.', {senderName}); return () => _t('%(senderName)s set a profile picture.', {senderName});
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) { } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
// This is a null rejoin, it will only be visible if the Labs option is enabled // This is a null rejoin, it will only be visible if the Labs option is enabled
return _t("%(senderName)s made no change.", {senderName}); return () => _t("%(senderName)s made no change.", {senderName});
} else { } else {
return ""; return null;
} }
} else { } else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
return _t('%(targetName)s joined the room.', {targetName}); return () => _t('%(targetName)s joined the room.', {targetName});
} }
case 'leave': case 'leave':
if (ev.getSender() === ev.getStateKey()) { if (ev.getSender() === ev.getStateKey()) {
if (prevContent.membership === "invite") { if (prevContent.membership === "invite") {
return _t('%(targetName)s rejected the invitation.', {targetName}); return () => _t('%(targetName)s rejected the invitation.', {targetName});
} else { } else {
return _t('%(targetName)s left the room.', {targetName}); return () => _t('%(targetName)s left the room.', {targetName});
} }
} else if (prevContent.membership === "ban") { } else if (prevContent.membership === "ban") {
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName}); return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
} else if (prevContent.membership === "invite") { } else if (prevContent.membership === "invite") {
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
senderName, senderName,
targetName, targetName,
}) + ' ' + reason; }) + ' ' + getReason();
} else if (prevContent.membership === "join") { } else if (prevContent.membership === "join") {
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason; return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
} else { } else {
return ""; return null;
} }
} }
} }
function textForTopicEvent(ev) { function textForTopicEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
senderDisplayName, senderDisplayName,
topic: ev.getContent().topic, topic: ev.getContent().topic,
}); });
} }
function textForRoomNameEvent(ev) { function textForRoomNameEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName}); return () => _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
} }
if (ev.getPrevContent().name) { if (ev.getPrevContent().name) {
return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', { return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
senderDisplayName, senderDisplayName,
oldRoomName: ev.getPrevContent().name, oldRoomName: ev.getPrevContent().name,
newRoomName: ev.getContent().name, newRoomName: ev.getContent().name,
}); });
} }
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', { return () => _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
senderDisplayName, senderDisplayName,
roomName: ev.getContent().name, roomName: ev.getContent().name,
}); });
} }
function textForTombstoneEvent(ev) { function textForTombstoneEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName}); return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
} }
function textForJoinRulesEvent(ev) { function textForJoinRulesEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().join_rule) { switch (ev.getContent().join_rule) {
case "public": case "public":
return _t('%(senderDisplayName)s made the room public to whoever knows the link.', {senderDisplayName}); return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
senderDisplayName,
});
case "invite": case "invite":
return _t('%(senderDisplayName)s made the room invite only.', {senderDisplayName}); return () => _t('%(senderDisplayName)s made the room invite only.', {
senderDisplayName,
});
default: default:
// The spec supports "knock" and "private", however nothing implements these. // The spec supports "knock" and "private", however nothing implements these.
return _t('%(senderDisplayName)s changed the join rule to %(rule)s', { return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
senderDisplayName, senderDisplayName,
rule: ev.getContent().join_rule, rule: ev.getContent().join_rule,
}); });
} }
} }
function textForGuestAccessEvent(ev) { function textForGuestAccessEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().guest_access) { switch (ev.getContent().guest_access) {
case "can_join": case "can_join":
return _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName}); return () => _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
case "forbidden": case "forbidden":
return _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName}); return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
default: default:
// There's no other options we can expect, however just for safety's sake we'll do this. // There's no other options we can expect, however just for safety's sake we'll do this.
return _t('%(senderDisplayName)s changed guest access to %(rule)s', { return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
senderDisplayName, senderDisplayName,
rule: ev.getContent().guest_access, rule: ev.getContent().guest_access,
}); });
} }
} }
function textForRelatedGroupsEvent(ev) { function textForRelatedGroupsEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const groups = ev.getContent().groups || []; const groups = ev.getContent().groups || [];
const prevGroups = ev.getPrevContent().groups || []; const prevGroups = ev.getPrevContent().groups || [];
@ -175,17 +183,17 @@ function textForRelatedGroupsEvent(ev) {
const removed = prevGroups.filter((g) => !groups.includes(g)); const removed = prevGroups.filter((g) => !groups.includes(g));
if (added.length && !removed.length) { if (added.length && !removed.length) {
return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', { return () => _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
senderDisplayName, senderDisplayName,
groups: added.join(', '), groups: added.join(', '),
}); });
} else if (!added.length && removed.length) { } else if (!added.length && removed.length) {
return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', { return () => _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
senderDisplayName, senderDisplayName,
groups: removed.join(', '), groups: removed.join(', '),
}); });
} else if (added.length && removed.length) { } else if (added.length && removed.length) {
return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' + return () => _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
'%(oldGroups)s in this room.', { '%(oldGroups)s in this room.', {
senderDisplayName, senderDisplayName,
newGroups: added.join(', '), newGroups: added.join(', '),
@ -193,11 +201,11 @@ function textForRelatedGroupsEvent(ev) {
}); });
} else { } else {
// Don't bother rendering this change (because there were no changes) // Don't bother rendering this change (because there were no changes)
return ''; return null;
} }
} }
function textForServerACLEvent(ev) { function textForServerACLEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const prevContent = ev.getPrevContent(); const prevContent = ev.getPrevContent();
const current = ev.getContent(); const current = ev.getContent();
@ -207,11 +215,11 @@ function textForServerACLEvent(ev) {
allow_ip_literals: !(prevContent.allow_ip_literals === false), allow_ip_literals: !(prevContent.allow_ip_literals === false),
}; };
let text = ""; let getText = null;
if (prev.deny.length === 0 && prev.allow.length === 0) { if (prev.deny.length === 0 && prev.allow.length === 0) {
text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName}); getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
} else { } else {
text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName}); getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
} }
if (!Array.isArray(current.allow)) { if (!Array.isArray(current.allow)) {
@ -220,13 +228,15 @@ function textForServerACLEvent(ev) {
// If we know for sure everyone is banned, mark the room as obliterated // If we know for sure everyone is banned, mark the room as obliterated
if (current.allow.length === 0) { if (current.allow.length === 0) {
return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used."); return () => getText() + " " +
_t("🎉 All servers are banned from participating! This room can no longer be used.");
} }
return text; return getText;
} }
function textForMessageEvent(ev) { function textForMessageEvent(ev): () => string | null {
return () => {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
let message = senderDisplayName + ': ' + ev.getContent().body; let message = senderDisplayName + ': ' + ev.getContent().body;
if (ev.getContent().msgtype === "m.emote") { if (ev.getContent().msgtype === "m.emote") {
@ -235,9 +245,10 @@ function textForMessageEvent(ev) {
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName}); message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
} }
return message; return message;
};
} }
function textForCanonicalAliasEvent(ev) { function textForCanonicalAliasEvent(ev): () => string | null {
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const oldAlias = ev.getPrevContent().alias; const oldAlias = ev.getPrevContent().alias;
const oldAltAliases = ev.getPrevContent().alt_aliases || []; const oldAltAliases = ev.getPrevContent().alt_aliases || [];
@ -248,96 +259,100 @@ function textForCanonicalAliasEvent(ev) {
if (!removedAltAliases.length && !addedAltAliases.length) { if (!removedAltAliases.length && !addedAltAliases.length) {
if (newAlias) { if (newAlias) {
return _t('%(senderName)s set the main address for this room to %(address)s.', { return () => _t('%(senderName)s set the main address for this room to %(address)s.', {
senderName: senderName, senderName: senderName,
address: ev.getContent().alias, address: ev.getContent().alias,
}); });
} else if (oldAlias) { } else if (oldAlias) {
return _t('%(senderName)s removed the main address for this room.', { return () => _t('%(senderName)s removed the main address for this room.', {
senderName: senderName, senderName: senderName,
}); });
} }
} else if (newAlias === oldAlias) { } else if (newAlias === oldAlias) {
if (addedAltAliases.length && !removedAltAliases.length) { if (addedAltAliases.length && !removedAltAliases.length) {
return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', { return () => _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
senderName: senderName, senderName: senderName,
addresses: addedAltAliases.join(", "), addresses: addedAltAliases.join(", "),
count: addedAltAliases.length, count: addedAltAliases.length,
}); });
} if (removedAltAliases.length && !addedAltAliases.length) { } if (removedAltAliases.length && !addedAltAliases.length) {
return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', { return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
senderName: senderName, senderName: senderName,
addresses: removedAltAliases.join(", "), addresses: removedAltAliases.join(", "),
count: removedAltAliases.length, count: removedAltAliases.length,
}); });
} if (removedAltAliases.length && addedAltAliases.length) { } if (removedAltAliases.length && addedAltAliases.length) {
return _t('%(senderName)s changed the alternative addresses for this room.', { return () => _t('%(senderName)s changed the alternative addresses for this room.', {
senderName: senderName, senderName: senderName,
}); });
} }
} else { } else {
// both alias and alt_aliases where modified // both alias and alt_aliases where modified
return _t('%(senderName)s changed the main and alternative addresses for this room.', { return () => _t('%(senderName)s changed the main and alternative addresses for this room.', {
senderName: senderName, senderName: senderName,
}); });
} }
// in case there is no difference between the two events, // in case there is no difference between the two events,
// say something as we can't simply hide the tile from here // say something as we can't simply hide the tile from here
return _t('%(senderName)s changed the addresses for this room.', { return () => _t('%(senderName)s changed the addresses for this room.', {
senderName: senderName, senderName: senderName,
}); });
} }
function textForCallAnswerEvent(event) { function textForCallAnswerEvent(event): () => string | null {
return () => {
const senderName = event.sender ? event.sender.name : _t('Someone'); const senderName = event.sender ? event.sender.name : _t('Someone');
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported; return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
};
} }
function textForCallHangupEvent(event) { function textForCallHangupEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : _t('Someone'); const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent(); const eventContent = event.getContent();
let reason = ""; let getReason = () => "";
if (!MatrixClientPeg.get().supportsVoip()) { if (!MatrixClientPeg.get().supportsVoip()) {
reason = _t('(not supported by this browser)'); getReason = () => _t('(not supported by this browser)');
} else if (eventContent.reason) { } else if (eventContent.reason) {
if (eventContent.reason === "ice_failed") { if (eventContent.reason === "ice_failed") {
// We couldn't establish a connection at all // We couldn't establish a connection at all
reason = _t('(could not connect media)'); getReason = () => _t('(could not connect media)');
} else if (eventContent.reason === "ice_timeout") { } else if (eventContent.reason === "ice_timeout") {
// We established a connection but it died // We established a connection but it died
reason = _t('(connection failed)'); getReason = () => _t('(connection failed)');
} else if (eventContent.reason === "user_media_failed") { } else if (eventContent.reason === "user_media_failed") {
// The other side couldn't open capture devices // The other side couldn't open capture devices
reason = _t("(their device couldn't start the camera / microphone)"); getReason = () => _t("(their device couldn't start the camera / microphone)");
} else if (eventContent.reason === "unknown_error") { } else if (eventContent.reason === "unknown_error") {
// An error code the other side doesn't have a way to express // An error code the other side doesn't have a way to express
// (as opposed to an error code they gave but we don't know about, // (as opposed to an error code they gave but we don't know about,
// in which case we show the error code) // in which case we show the error code)
reason = _t("(an error occurred)"); getReason = () => _t("(an error occurred)");
} else if (eventContent.reason === "invite_timeout") { } else if (eventContent.reason === "invite_timeout") {
reason = _t('(no answer)'); getReason = () => _t('(no answer)');
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") { } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
// workaround for https://github.com/vector-im/element-web/issues/5178 // workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is // it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :( // interpreted as an error code :(
// https://github.com/vector-im/riot-android/issues/2623 // https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore) // Also the correct hangup code as of VoIP v1 (with underscore)
reason = ''; getReason = () => '';
} else { } else {
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason}); getReason = () => _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
} }
} }
return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason; return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
} }
function textForCallRejectEvent(event) { function textForCallRejectEvent(event): () => string | null {
return () => {
const senderName = event.sender ? event.sender.name : _t('Someone'); const senderName = event.sender ? event.sender.name : _t('Someone');
return _t('%(senderName)s declined the call.', {senderName}); return _t('%(senderName)s declined the call.', {senderName});
};
} }
function textForCallInviteEvent(event) { function textForCallInviteEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : _t('Someone'); const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event? // FIXME: Find a better way to determine this from the event?
let isVoice = true; let isVoice = true;
if (event.getContent().offer && event.getContent().offer.sdp && if (event.getContent().offer && event.getContent().offer.sdp &&
@ -350,48 +365,55 @@ function textForCallInviteEvent(event) {
// can have a hard time translating those strings. In an effort to make translations easier // can have a hard time translating those strings. In an effort to make translations easier
// and more accurate, we break out the string-based variables to a couple booleans. // and more accurate, we break out the string-based variables to a couple booleans.
if (isVoice && isSupported) { if (isVoice && isSupported) {
return _t("%(senderName)s placed a voice call.", {senderName}); return () => _t("%(senderName)s placed a voice call.", {
senderName: getSenderName(),
});
} else if (isVoice && !isSupported) { } else if (isVoice && !isSupported) {
return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName}); return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
senderName: getSenderName(),
});
} else if (!isVoice && isSupported) { } else if (!isVoice && isSupported) {
return _t("%(senderName)s placed a video call.", {senderName}); return () => _t("%(senderName)s placed a video call.", {
senderName: getSenderName(),
});
} else if (!isVoice && !isSupported) { } else if (!isVoice && !isSupported) {
return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName}); return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
senderName: getSenderName(),
});
} }
} }
function textForThreePidInviteEvent(event) { function textForThreePidInviteEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
if (!isValid3pidInvite(event)) { if (!isValid3pidInvite(event)) {
const targetDisplayName = event.getPrevContent().display_name || _t("Someone"); return () => _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
senderName, senderName,
targetDisplayName, targetDisplayName: event.getPrevContent().display_name || _t("Someone"),
}); });
} }
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', { return () => _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
senderName, senderName,
targetDisplayName: event.getContent().display_name, targetDisplayName: event.getContent().display_name,
}); });
} }
function textForHistoryVisibilityEvent(event) { function textForHistoryVisibilityEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
switch (event.getContent().history_visibility) { switch (event.getContent().history_visibility) {
case 'invited': case 'invited':
return _t('%(senderName)s made future room history visible to all room members, ' return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they are invited.', {senderName}); + 'from the point they are invited.', {senderName});
case 'joined': case 'joined':
return _t('%(senderName)s made future room history visible to all room members, ' return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they joined.', {senderName}); + 'from the point they joined.', {senderName});
case 'shared': case 'shared':
return _t('%(senderName)s made future room history visible to all room members.', {senderName}); return () => _t('%(senderName)s made future room history visible to all room members.', {senderName});
case 'world_readable': case 'world_readable':
return _t('%(senderName)s made future room history visible to anyone.', {senderName}); return () => _t('%(senderName)s made future room history visible to anyone.', {senderName});
default: default:
return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', { return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
senderName, senderName,
visibility: event.getContent().history_visibility, visibility: event.getContent().history_visibility,
}); });
@ -399,11 +421,11 @@ function textForHistoryVisibilityEvent(event) {
} }
// Currently will only display a change if a user's power level is changed // Currently will only display a change if a user's power level is changed
function textForPowerEvent(event) { function textForPowerEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
if (!event.getPrevContent() || !event.getPrevContent().users || if (!event.getPrevContent() || !event.getPrevContent().users ||
!event.getContent() || !event.getContent().users) { !event.getContent() || !event.getContent().users) {
return ''; return null;
} }
const userDefault = event.getContent().users_default || 0; const userDefault = event.getContent().users_default || 0;
// Construct set of userIds // Construct set of userIds
@ -418,38 +440,38 @@ function textForPowerEvent(event) {
if (users.indexOf(userId) === -1) users.push(userId); if (users.indexOf(userId) === -1) users.push(userId);
}, },
); );
const diff = []; const diffs = [];
// XXX: This is also surely broken for i18n
users.forEach((userId) => { users.forEach((userId) => {
// Previous power level // Previous power level
const from = event.getPrevContent().users[userId]; const from = event.getPrevContent().users[userId];
// Current power level // Current power level
const to = event.getContent().users[userId]; const to = event.getContent().users[userId];
if (to !== from) { if (to !== from) {
diff.push( diffs.push({ userId, from, to });
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId,
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
}),
);
} }
}); });
if (!diff.length) { if (!diffs.length) {
return ''; return null;
} }
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { // XXX: This is also surely broken for i18n
return () => _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName, senderName,
powerLevelDiffText: diff.join(", "), powerLevelDiffText: diffs.map(diff =>
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: diff.userId,
fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault),
toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault),
}),
).join(", "),
}); });
} }
function textForPinnedEvent(event) { function textForPinnedEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
return _t("%(senderName)s changed the pinned messages for the room.", {senderName}); return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
} }
function textForWidgetEvent(event) { function textForWidgetEvent(event): () => string | null {
const senderName = event.getSender(); const senderName = event.getSender();
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent(); const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
const {name, type, url} = event.getContent() || {}; const {name, type, url} = event.getContent() || {};
@ -464,27 +486,27 @@ function textForWidgetEvent(event) {
// equivalent to that condition. // equivalent to that condition.
if (url) { if (url) {
if (prevUrl) { if (prevUrl) {
return _t('%(widgetName)s widget modified by %(senderName)s', { return () => _t('%(widgetName)s widget modified by %(senderName)s', {
widgetName, senderName, widgetName, senderName,
}); });
} else { } else {
return _t('%(widgetName)s widget added by %(senderName)s', { return () => _t('%(widgetName)s widget added by %(senderName)s', {
widgetName, senderName, widgetName, senderName,
}); });
} }
} else { } else {
return _t('%(widgetName)s widget removed by %(senderName)s', { return () => _t('%(widgetName)s widget removed by %(senderName)s', {
widgetName, senderName, widgetName, senderName,
}); });
} }
} }
function textForWidgetLayoutEvent(event) { function textForWidgetLayoutEvent(event): () => string | null {
const senderName = event.sender?.name || event.getSender(); const senderName = event.sender?.name || event.getSender();
return _t("%(senderName)s has updated the widget layout", {senderName}); return () => _t("%(senderName)s has updated the widget layout", {senderName});
} }
function textForMjolnirEvent(event) { function textForMjolnirEvent(event): () => string | null {
const senderName = event.getSender(); const senderName = event.getSender();
const {entity: prevEntity} = event.getPrevContent(); const {entity: prevEntity} = event.getPrevContent();
const {entity, recommendation, reason} = event.getContent(); const {entity, recommendation, reason} = event.getContent();
@ -492,74 +514,74 @@ function textForMjolnirEvent(event) {
// Rule removed // Rule removed
if (!entity) { if (!entity) {
if (USER_RULE_TYPES.includes(event.getType())) { if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s removed the rule banning users matching %(glob)s", return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
{senderName, glob: prevEntity}); {senderName, glob: prevEntity});
} else if (ROOM_RULE_TYPES.includes(event.getType())) { } else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s removed the rule banning rooms matching %(glob)s", return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
{senderName, glob: prevEntity}); {senderName, glob: prevEntity});
} else if (SERVER_RULE_TYPES.includes(event.getType())) { } else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s removed the rule banning servers matching %(glob)s", return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
{senderName, glob: prevEntity}); {senderName, glob: prevEntity});
} }
// Unknown type. We'll say something, but we shouldn't end up here. // Unknown type. We'll say something, but we shouldn't end up here.
return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity}); return () => _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
} }
// Invalid rule // Invalid rule
if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName}); if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, {senderName});
// Rule updated // Rule updated
if (entity === prevEntity) { if (entity === prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) { if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s", return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} else if (ROOM_RULE_TYPES.includes(event.getType())) { } else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s", return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} else if (SERVER_RULE_TYPES.includes(event.getType())) { } else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s", return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} }
// Unknown type. We'll say something but we shouldn't end up here. // Unknown type. We'll say something but we shouldn't end up here.
return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s", return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} }
// New rule // New rule
if (!prevEntity) { if (!prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) { if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s", return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} else if (ROOM_RULE_TYPES.includes(event.getType())) { } else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s", return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} else if (SERVER_RULE_TYPES.includes(event.getType())) { } else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s", return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} }
// Unknown type. We'll say something but we shouldn't end up here. // Unknown type. We'll say something but we shouldn't end up here.
return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s", return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} }
// else the entity !== prevEntity - count as a removal & add // else the entity !== prevEntity - count as a removal & add
if (USER_RULE_TYPES.includes(event.getType())) { if (USER_RULE_TYPES.includes(event.getType())) {
return _t( return () => _t(
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s", "%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason}, {senderName, oldGlob: prevEntity, newGlob: entity, reason},
); );
} else if (ROOM_RULE_TYPES.includes(event.getType())) { } else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t( return () => _t(
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s", "%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason}, {senderName, oldGlob: prevEntity, newGlob: entity, reason},
); );
} else if (SERVER_RULE_TYPES.includes(event.getType())) { } else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t( return () => _t(
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s", "%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason}, {senderName, oldGlob: prevEntity, newGlob: entity, reason},
@ -567,11 +589,15 @@ function textForMjolnirEvent(event) {
} }
// Unknown type. We'll say something but we shouldn't end up here. // Unknown type. We'll say something but we shouldn't end up here.
return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " + return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason}); "for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
} }
const handlers = { interface IHandlers {
[type: string]: (ev: any) => (() => string | null);
}
const handlers: IHandlers = {
'm.room.message': textForMessageEvent, 'm.room.message': textForMessageEvent,
'm.call.invite': textForCallInviteEvent, 'm.call.invite': textForCallInviteEvent,
'm.call.answer': textForCallAnswerEvent, 'm.call.answer': textForCallAnswerEvent,
@ -579,7 +605,7 @@ const handlers = {
'm.call.reject': textForCallRejectEvent, 'm.call.reject': textForCallRejectEvent,
}; };
const stateHandlers = { const stateHandlers: IHandlers = {
'm.room.canonical_alias': textForCanonicalAliasEvent, 'm.room.canonical_alias': textForCanonicalAliasEvent,
'm.room.name': textForRoomNameEvent, 'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent, 'm.room.topic': textForTopicEvent,
@ -604,8 +630,12 @@ for (const evType of ALL_RULE_TYPES) {
stateHandlers[evType] = textForMjolnirEvent; stateHandlers[evType] = textForMjolnirEvent;
} }
export function textForEvent(ev) { export function hasText(ev): boolean {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
if (handler) return handler(ev); return Boolean(handler?.(ev));
return ''; }
export function textForEvent(ev): string {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
return handler?.(ev)?.() || '';
} }

View file

@ -24,13 +24,16 @@ import { HostSignupStore } from "../../stores/HostSignupStore";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps {} interface IProps {
onClick?(): void;
}
interface IState {} interface IState {}
@replaceableComponent("structures.HostSignupAction") @replaceableComponent("structures.HostSignupAction")
export default class HostSignupAction extends React.PureComponent<IProps, IState> { export default class HostSignupAction extends React.PureComponent<IProps, IState> {
private openDialog = async () => { private openDialog = async () => {
this.props.onClick?.();
await HostSignupStore.instance.setHostSignupActive(true); await HostSignupStore.instance.setHostSignupActive(true);
} }

View file

@ -25,10 +25,11 @@ import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore'; import SettingsStore from '../../settings/SettingsStore';
import RoomContext from "../../contexts/RoomContext";
import {Layout, LayoutPropType} from "../../settings/Layout"; import {Layout, LayoutPropType} from "../../settings/Layout";
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile"; import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent"; import {hasText} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap"; import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro"; import NewRoomIntro from "../views/rooms/NewRoomIntro";
@ -151,6 +152,8 @@ export default class MessagePanel extends React.Component {
enableFlair: PropTypes.bool, enableFlair: PropTypes.bool,
}; };
static contextType = RoomContext;
constructor(props) { constructor(props) {
super(props); super(props);
@ -380,7 +383,7 @@ export default class MessagePanel extends React.Component {
// Always show highlighted event // Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true; if (this.props.highlightedEventId === mxEv.getId()) return true;
return !shouldHideEvent(mxEv); return !shouldHideEvent(mxEv, this.context);
} }
_readMarkerForEvent(eventId, isLastEvent) { _readMarkerForEvent(eventId, isLastEvent) {
@ -1164,11 +1167,8 @@ class MemberGrouper {
add(ev) { add(ev) {
if (ev.getType() === 'm.room.member') { if (ev.getType() === 'm.room.member') {
// We'll just double check that it's worth our time to do so, through an // We can ignore any events that don't actually have a message to display
// ugly hack. If textForEvent returns something, we should group it for if (!hasText(ev)) return;
// rendering but if it doesn't then we'll exclude it.
const renderText = textForEvent(ev);
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
} }
this.readMarker = this.readMarker || this.panel._readMarkerForEvent( this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
ev.getId(), ev.getId(),

View file

@ -50,6 +50,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
showUrlPreview={false} showUrlPreview={false}
tileShape="notif" tileShape="notif"
empty={emptyState} empty={emptyState}
alwaysShowTimestamps={true}
/> />
); );
} else { } else {

View file

@ -59,7 +59,6 @@ import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary"; import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
import ForwardMessage from "../views/rooms/ForwardMessage";
import SearchBar from "../views/rooms/SearchBar"; import SearchBar from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel"; import AuxPanel from "../views/rooms/AuxPanel";
@ -136,7 +135,6 @@ export interface IState {
// Whether to highlight the event scrolled to // Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean; isInitialEventHighlighted?: boolean;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
forwardingEvent?: MatrixEvent;
numUnreadMessages: number; numUnreadMessages: number;
draggingFile: boolean; draggingFile: boolean;
searching: boolean; searching: boolean;
@ -155,7 +153,6 @@ export interface IState {
canPeek: boolean; canPeek: boolean;
showApps: boolean; showApps: boolean;
isPeeking: boolean; isPeeking: boolean;
showReadReceipts: boolean;
showRightPanel: boolean; showRightPanel: boolean;
// error object, as from the matrix client/server API // error object, as from the matrix client/server API
// If we failed to load information about the room, // If we failed to load information about the room,
@ -183,6 +180,12 @@ export interface IState {
canReact: boolean; canReact: boolean;
canReply: boolean; canReply: boolean;
layout: Layout; layout: Layout;
lowBandwidth: boolean;
showReadReceipts: boolean;
showRedactions: boolean;
showJoinLeaves: boolean;
showAvatarChanges: boolean;
showDisplaynameChanges: boolean;
matrixClientIsReady: boolean; matrixClientIsReady: boolean;
showUrlPreview?: boolean; showUrlPreview?: boolean;
e2eStatus?: E2EStatus; e2eStatus?: E2EStatus;
@ -200,8 +203,7 @@ export default class RoomView extends React.Component<IProps, IState> {
private readonly dispatcherRef: string; private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription; private readonly roomStoreToken: EventSubscription;
private readonly rightPanelStoreToken: EventSubscription; private readonly rightPanelStoreToken: EventSubscription;
private readonly showReadReceiptsWatchRef: string; private settingWatchers: string[];
private readonly layoutWatcherRef: string;
private unmounted = false; private unmounted = false;
private permalinkCreators: Record<string, RoomPermalinkCreator> = {}; private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
@ -232,7 +234,6 @@ export default class RoomView extends React.Component<IProps, IState> {
canPeek: false, canPeek: false,
showApps: false, showApps: false,
isPeeking: false, isPeeking: false,
showReadReceipts: true,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
joining: false, joining: false,
atEndOfLiveTimeline: true, atEndOfLiveTimeline: true,
@ -242,6 +243,12 @@ export default class RoomView extends React.Component<IProps, IState> {
canReact: false, canReact: false,
canReply: false, canReply: false,
layout: SettingsStore.getValue("layout"), layout: SettingsStore.getValue("layout"),
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
dragCounter: 0, dragCounter: 0,
}; };
@ -268,9 +275,14 @@ export default class RoomView extends React.Component<IProps, IState> {
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate); WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null, this.settingWatchers = [
this.onReadReceiptsChange); SettingsStore.watchSetting("layout", null, () =>
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange); this.setState({ layout: SettingsStore.getValue("layout") }),
),
SettingsStore.watchSetting("lowBandwidth", null, () =>
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
),
];
} }
private onWidgetStoreUpdate = () => { private onWidgetStoreUpdate = () => {
@ -323,13 +335,45 @@ export default class RoomView extends React.Component<IProps, IState> {
initialEventId: RoomViewStore.getInitialEventId(), initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(), replyToEvent: RoomViewStore.getQuotingEvent(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
// we should only peek once we have a ready client // we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
showRedactions: SettingsStore.getValue("showRedactions", roomId),
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(), wasContextSwitch: RoomViewStore.getWasContextSwitch(),
}; };
// Add watchers for each of the settings we just looked up
this.settingWatchers = this.settingWatchers.concat([
SettingsStore.watchSetting("showReadReceipts", null, () =>
this.setState({
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
}),
),
SettingsStore.watchSetting("showRedactions", null, () =>
this.setState({
showRedactions: SettingsStore.getValue("showRedactions", roomId),
}),
),
SettingsStore.watchSetting("showJoinLeaves", null, () =>
this.setState({
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
}),
),
SettingsStore.watchSetting("showAvatarChanges", null, () =>
this.setState({
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
}),
),
SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
this.setState({
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
}),
),
]);
if (!initial && this.state.shouldPeek && !newState.shouldPeek) { if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now // Stop peeking because we have joined this room now
this.context.stopPeeking(); this.context.stopPeeking();
@ -638,10 +682,6 @@ export default class RoomView extends React.Component<IProps, IState> {
); );
} }
if (this.showReadReceiptsWatchRef) {
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
}
// cancel any pending calls to the rate_limited_funcs // cancel any pending calls to the rate_limited_funcs
this.updateRoomMembers.cancelPendingCall(); this.updateRoomMembers.cancelPendingCall();
@ -649,7 +689,9 @@ export default class RoomView extends React.Component<IProps, IState> {
// console.log("Tinter.tint from RoomView.unmount"); // console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme // Tinter.tint(); // reset colourscheme
SettingsStore.unwatchSetting(this.layoutWatcherRef); for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
}
} }
private onUserScroll = () => { private onUserScroll = () => {
@ -819,7 +861,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// update unread count when scrolled up // update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change // no change
} else if (!shouldHideEvent(ev)) { } else if (!shouldHideEvent(ev, this.state)) {
this.setState((state, props) => { this.setState((state, props) => {
return {numUnreadMessages: state.numUnreadMessages + 1}; return {numUnreadMessages: state.numUnreadMessages + 1};
}); });
@ -1410,18 +1452,6 @@ export default class RoomView extends React.Component<IProps, IState> {
dis.dispatch({ action: "open_room_settings" }); dis.dispatch({ action: "open_room_settings" });
}; };
private onCancelClick = () => {
console.log("updateTint from onCancelClick");
this.updateTint();
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'forward_event',
event: null,
});
}
dis.fire(Action.FocusComposer);
};
private onAppsClick = () => { private onAppsClick = () => {
dis.dispatch({ dis.dispatch({
action: "appsDrawer", action: "appsDrawer",
@ -1837,11 +1867,7 @@ export default class RoomView extends React.Component<IProps, IState> {
let aux = null; let aux = null;
let previewBar; let previewBar;
let hideCancel = false; if (this.state.searching) {
if (this.state.forwardingEvent) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = <SearchBar aux = <SearchBar
searchInProgress={this.state.searchInProgress} searchInProgress={this.state.searchInProgress}
onCancelClick={this.onCancelSearchClick} onCancelClick={this.onCancelSearchClick}
@ -1850,7 +1876,6 @@ export default class RoomView extends React.Component<IProps, IState> {
/>; />;
} else if (showRoomUpgradeBar) { } else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />; aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
hideCancel = true;
} else if (myMembership !== "join") { } else if (myMembership !== "join") {
// We do have a room object for this room, but we're not currently in it. // We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it. // We may have a 3rd party invite to it.
@ -1859,7 +1884,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inviterName = this.props.oobData.inviterName; inviterName = this.props.oobData.inviterName;
} }
const invitedEmail = this.props.threepidInvite?.toEmail; const invitedEmail = this.props.threepidInvite?.toEmail;
hideCancel = true;
previewBar = ( previewBar = (
<RoomPreviewBar <RoomPreviewBar
onJoinClick={this.onJoinButtonClicked} onJoinClick={this.onJoinButtonClicked}
@ -1977,11 +2001,8 @@ export default class RoomView extends React.Component<IProps, IState> {
hideMessagePanel = true; hideMessagePanel = true;
} }
const shouldHighlight = this.state.isInitialEventHighlighted;
let highlightedEventId = null; let highlightedEventId = null;
if (this.state.forwardingEvent) { if (this.state.isInitialEventHighlighted) {
highlightedEventId = this.state.forwardingEvent.getId();
} else if (shouldHighlight) {
highlightedEventId = this.state.initialEventId; highlightedEventId = this.state.initialEventId;
} }
@ -2070,7 +2091,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inRoom={myMembership === 'join'} inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus} e2eStatus={this.state.e2eStatus}

View file

@ -587,6 +587,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
<h3>{ _t("Me and my teammates") }</h3> <h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div> <div>{ _t("A private space for you and your teammates") }</div>
</AccessibleButton> </AccessibleButton>
<div className="mx_SpaceRoomView_betaWarning">
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
</div>
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
</div>; </div>;
}; };

View file

@ -26,6 +26,7 @@ import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window"; import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg"; import {MatrixClientPeg} from "../../MatrixClientPeg";
import RoomContext from "../../contexts/RoomContext";
import UserActivity from "../../UserActivity"; import UserActivity from "../../UserActivity";
import Modal from "../../Modal"; import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
@ -120,8 +121,13 @@ class TimelinePanel extends React.Component {
// which layout to use // which layout to use
layout: LayoutPropType, layout: LayoutPropType,
// whether to always show timestamps for an event
alwaysShowTimestamps: PropTypes.bool,
} }
static contextType = RoomContext;
// a map from room id to read marker event timestamp // a map from room id to read marker event timestamp
static roomReadMarkerTsMap = {}; static roomReadMarkerTsMap = {};
@ -1285,7 +1291,7 @@ class TimelinePanel extends React.Component {
const shouldIgnore = !!ev.status || // local echo const shouldIgnore = !!ev.status || // local echo
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message (ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev); const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
if (isWithoutTile || !node) { if (isWithoutTile || !node) {
// don't start counting if the event should be ignored, // don't start counting if the event should be ignored,
@ -1440,7 +1446,7 @@ class TimelinePanel extends React.Component {
onFillRequest={this.onMessageListFillRequest} onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest} onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.state.isTwelveHour} isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={this.state.alwaysShowTimestamps} alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
className={this.props.className} className={this.props.className}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}

View file

@ -366,9 +366,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
const mxDomain = MatrixClientPeg.get().getDomain(); const mxDomain = MatrixClientPeg.get().getDomain();
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`))); const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
if (!hostSignupConfig.domains || validDomains.length > 0) { if (!hostSignupConfig.domains || validDomains.length > 0) {
topSection = <div onClick={this.onCloseMenu}> topSection = <HostSignupAction onClick={this.onCloseMenu} />;
<HostSignupAction />
</div>;
} }
} }
} }

View file

@ -22,6 +22,7 @@ import classNames from 'classnames';
import * as AvatarLogic from '../../../Avatar'; import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units"; import {toPx} from "../../../utils/units";
@ -44,12 +45,12 @@ interface IProps {
className?: string; className?: string;
} }
const calculateUrls = (url, urls) => { const calculateUrls = (url, urls, lowBandwidth) => {
// work out the full set of urls to try to load. This is formed like so: // work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ] // imageUrls: [ props.url, ...props.urls ]
let _urls = []; let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) { if (!lowBandwidth) {
_urls = urls || []; _urls = urls || [];
if (url) { if (url) {
@ -63,7 +64,13 @@ const calculateUrls = (url, urls) => {
}; };
const useImageUrl = ({url, urls}): [string, () => void] => { const useImageUrl = ({url, urls}): [string, () => void] => {
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls)); // Since this is a hot code path and the settings store can be slow, we
// use the cached lowBandwidth value from the room context if it exists
const roomContext = useContext(RoomContext);
const lowBandwidth = roomContext ?
roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
const [urlsIndex, setIndex] = useState<number>(0); const [urlsIndex, setIndex] = useState<number>(0);
const onError = useCallback(() => { const onError = useCallback(() => {
@ -71,7 +78,7 @@ const useImageUrl = ({url, urls}): [string, () => void] => {
}, []); }, []);
useEffect(() => { useEffect(() => {
setUrls(calculateUrls(url, urls)); setUrls(calculateUrls(url, urls, lowBandwidth));
setIndex(0); setIndex(0);
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps

View file

@ -32,6 +32,7 @@ import { MenuItem } from "../../structures/ContextMenu";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
export function canCancel(eventStatus) { export function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -157,10 +158,10 @@ export default class MessageContextMenu extends React.Component {
}; };
onForwardClick = () => { onForwardClick = () => {
if (this.props.onCloseDialog) this.props.onCloseDialog(); Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
dis.dispatch({ matrixClient: MatrixClientPeg.get(),
action: 'forward_event',
event: this.props.mxEvent, event: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
}); });
this.closeMenu(); this.closeMenu();
}; };

View file

@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
showUnpin?: boolean; showUnpin?: boolean;
// override delete handler // override delete handler
onDeleteClick?(): void; onDeleteClick?(): void;
// override edit handler
onEditClick?(): void;
} }
const WidgetContextMenu: React.FC<IProps> = ({ const WidgetContextMenu: React.FC<IProps> = ({
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
app, app,
userWidget, userWidget,
onDeleteClick, onDeleteClick,
onEditClick,
showUnpin, showUnpin,
...props ...props
}) => { }) => {
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
let editButton; let editButton;
if (canModify && WidgetUtils.isManagedByManager(app)) { if (canModify && WidgetUtils.isManagedByManager(app)) {
const onEditClick = () => { const _onEditClick = () => {
if (onEditClick) {
onEditClick();
} else {
WidgetUtils.editWidget(room, app); WidgetUtils.editWidget(room, app);
}
onFinished(); onFinished();
}; };
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />; editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
} }
let snapshotButton; let snapshotButton;
@ -116,7 +123,10 @@ const WidgetContextMenu: React.FC<IProps> = ({
let deleteButton; let deleteButton;
if (onDeleteClick || canModify) { if (onDeleteClick || canModify) {
const onDeleteClickDefault = () => { const _onDeleteClick = () => {
if (onDeleteClick) {
onDeleteClick();
} else {
// Show delete confirmation dialog // Show delete confirmation dialog
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, { Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
title: _t("Delete Widget"), title: _t("Delete Widget"),
@ -129,11 +139,13 @@ const WidgetContextMenu: React.FC<IProps> = ({
WidgetUtils.setRoomWidget(roomId, app.id); WidgetUtils.setRoomWidget(roomId, app.id);
}, },
}); });
}
onFinished(); onFinished();
}; };
deleteButton = <IconizedContextMenuOption deleteButton = <IconizedContextMenuOption
onClick={onDeleteClick || onDeleteClickDefault} onClick={_onDeleteClick}
label={userWidget ? _t("Remove") : _t("Remove for everyone")} label={userWidget ? _t("Remove") : _t("Remove for everyone")}
/>; />;
} }

View file

@ -0,0 +1,247 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
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, {useMemo, useState, useEffect} from "react";
import classnames from "classnames";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {_t} from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
import {UIFeature} from "../../../settings/UIFeature";
import {Layout} from "../../../settings/Layout";
import {IDialogProps} from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import {avatarUrlForUser} from "../../../Avatar";
import EventTile from "../rooms/EventTile";
import SearchBox from "../../structures/SearchBox";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {Alignment} from '../elements/Tooltip';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "../rooms/NotificationBadge";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
const AVATAR_SIZE = 30;
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
// The event to forward
event: MatrixEvent;
// We need a permalink creator for the source room to pass through to EventTile
// in case the event is a reply (even though the user can't get at the link)
permalinkCreator: RoomPermalinkCreator;
}
interface IEntryProps {
room: Room;
event: MatrixEvent;
matrixClient: MatrixClient;
onFinished(success: boolean): void;
}
enum SendState {
CanSend,
Sending,
Sent,
Failed,
}
const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
const jumpToRoom = () => {
dis.dispatch({
action: "view_room",
room_id: room.roomId,
});
onFinished(true);
};
const send = async () => {
setSendState(SendState.Sending);
try {
await cli.sendEvent(room.roomId, event.getType(), event.getContent());
setSendState(SendState.Sent);
} catch (e) {
setSendState(SendState.Failed);
}
};
let className;
let disabled = false;
let title;
let icon;
if (sendState === SendState.CanSend) {
className = "mx_ForwardList_canSend";
if (room.maySendMessage()) {
title = _t("Send");
} else {
disabled = true;
title = _t("You don't have permission to do this");
}
} else if (sendState === SendState.Sending) {
className = "mx_ForwardList_sending";
disabled = true;
title = _t("Sending");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
} else if (sendState === SendState.Sent) {
className = "mx_ForwardList_sent";
disabled = true;
title = _t("Sent");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
} else {
className = "mx_ForwardList_sendFailed";
disabled = true;
title = _t("Failed to send");
icon = <NotificationBadge
notification={StaticNotificationState.RED_EXCLAMATION}
/>;
}
return <div className="mx_ForwardList_entry">
<AccessibleTooltipButton
className="mx_ForwardList_roomButton"
onClick={jumpToRoom}
title={_t("Open link")}
yOffset={-20}
alignment={Alignment.Top}
>
<DecoratedRoomAvatar room={room} avatarSize={32} />
<span className="mx_ForwardList_entry_name">{ room.name }</span>
</AccessibleTooltipButton>
<AccessibleTooltipButton
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
className={`mx_ForwardList_sendButton ${className}`}
onClick={send}
disabled={disabled}
title={title}
yOffset={-20}
alignment={Alignment.Top}
>
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
{ icon }
</AccessibleTooltipButton>
</div>;
};
const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
const userId = cli.getUserId();
const [profileInfo, setProfileInfo] = useState<any>({});
useEffect(() => {
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
}, [cli, userId]);
// For the message preview we fake the sender as ourselves
const mockEvent = new MatrixEvent({
type: "m.room.message",
sender: userId,
content: event.getContent(),
unsigned: {
age: 97,
},
event_id: "$9999999999999999999999999999999999999999999",
room_id: event.getRoomId(),
});
mockEvent.sender = {
name: profileInfo.displayname || userId,
userId,
getAvatarUrl: (..._) => {
return avatarUrlForUser(
{ avatarUrl: profileInfo.avatar_url },
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => profileInfo.avatar_url,
};
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
const spacesEnabled = useFeatureEnabled("feature_spaces");
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
const previewLayout = useSettingValue<Layout>("layout");
let rooms = useMemo(() => sortRooms(
cli.getVisibleRooms().filter(
room => room.getMyMembership() === "join" &&
!(spacesEnabled && room.isSpaceRoom()),
),
), [cli, spacesEnabled]);
if (lcQuery) {
rooms = new QueryMatcher<Room>(rooms, {
keys: ["name"],
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
shouldMatchWordsOnly: false,
}).match(lcQuery);
}
return <BaseDialog
title={_t("Forward message")}
className="mx_ForwardDialog"
contentId="mx_ForwardList"
onFinished={onFinished}
fixedWidth={false}
>
<h3>{ _t("Message preview") }</h3>
<div className={classnames("mx_ForwardDialog_preview", {
"mx_IRCLayout": previewLayout == Layout.IRC,
"mx_GroupLayout": previewLayout == Layout.Group,
})}>
<EventTile
mxEvent={mockEvent}
layout={previewLayout}
enableFlair={flairEnabled}
permalinkCreator={permalinkCreator}
as="div"
/>
</div>
<hr />
<div className="mx_ForwardList" id="mx_ForwardList">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Search for rooms or people")}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ForwardList_content">
{ rooms.length > 0 ? (
<div className="mx_ForwardList_results">
{ rooms.map(room =>
<Entry
key={room.roomId}
room={room}
event={event}
matrixClient={cli}
onFinished={onFinished}
/>,
) }
</div>
) : <span className="mx_ForwardList_noResults">
{ _t("No results") }
</span> }
</AutoHideScrollbar>
</div>
</BaseDialog>;
};
export default ForwardDialog;

View file

@ -19,7 +19,7 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip'; import Tooltip, {Alignment} from './Tooltip';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> { interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
tooltipClassName?: string; tooltipClassName?: string;
forceHide?: boolean; forceHide?: boolean;
yOffset?: number; yOffset?: number;
alignment?: Alignment;
} }
interface IState { interface IState {
@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
render() { render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props; const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
const tip = this.state.hover ? <Tooltip const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container" className="mx_AccessibleTooltipButton_container"
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)} tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
label={tooltip || title} label={tooltip || title}
yOffset={yOffset} yOffset={yOffset}
alignment={alignment}
/> : null; /> : null;
return ( return (
<AccessibleButton <AccessibleButton

View file

@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
// The key used for PersistedElement // The key used for PersistedElement
this._persistKey = getPersistKey(this.props.app.id); this._persistKey = getPersistKey(this.props.app.id);
try {
this._sgWidget = new StopGapWidget(this.props); this._sgWidget = new StopGapWidget(this.props);
this._sgWidget.on("preparing", this._onWidgetPrepared); this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this._sgWidget.on("ready", this._onWidgetReady);
} catch (e) {
console.log("Failed to construct widget", e);
this._sgWidget = null;
}
this.iframe = null; // ref to the iframe (callback style) this.iframe = null; // ref to the iframe (callback style)
this.state = this._getNewState(props); this.state = this._getNewState(props);
@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
// Force the widget to be non-persistent (able to be deleted/forgotten) // Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this._persistKey);
this._sgWidget.stop(); if (this._sgWidget) this._sgWidget.stop();
} }
this.setState({ hasPermissionToLoad }); this.setState({ hasPermissionToLoad });
@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
componentDidMount() { componentDidMount() {
// Only fetch IM token on mount if we're showing and have permission to load // Only fetch IM token on mount if we're showing and have permission to load
if (this.state.hasPermissionToLoad) { if (this._sgWidget && this.state.hasPermissionToLoad) {
this._startWidget(); this._startWidget();
} }
@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
if (this._sgWidget) { if (this._sgWidget) {
this._sgWidget.stop(); this._sgWidget.stop();
} }
try {
this._sgWidget = new StopGapWidget(newProps); this._sgWidget = new StopGapWidget(newProps);
this._sgWidget.on("preparing", this._onWidgetPrepared); this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this._sgWidget.on("ready", this._onWidgetReady);
this._startWidget(); this._startWidget();
} catch (e) {
console.log("Failed to construct widget", e);
this._sgWidget = null;
}
} }
_startWidget() { _startWidget() {
@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
_iframeRefChange = (ref) => { _iframeRefChange = (ref) => {
this.iframe = ref; this.iframe = ref;
if (ref) { if (ref) {
this._sgWidget.start(ref); if (this._sgWidget) this._sgWidget.start(ref);
} else { } else {
this._resetWidget(this.props); this._resetWidget(this.props);
} }
@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this._persistKey);
this._sgWidget.stop({forceDestroy: true}); if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
} }
_onWidgetPrepared = () => { _onWidgetPrepared = () => {
@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
<Spinner message={_t("Loading...")} /> <Spinner message={_t("Loading...")} />
</div> </div>
); );
if (!this.state.hasPermissionToLoad) { if (this._sgWidget === null) {
appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg={_t("Error loading Widget")} />
</div>
);
} else if (!this.state.hasPermissionToLoad) {
// only possible for room widgets, can assert this.props.room here // only possible for room widgets, can assert this.props.room here
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = ( appTileBody = (
@ -364,7 +380,7 @@ export default class AppTile extends React.Component {
if (this.isMixedContent()) { if (this.isMixedContent()) {
appTileBody = ( appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}> <div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg="Error - Mixed content" /> <AppWarning errorMsg={_t("Error - Mixed content")} />
</div> </div>
); );
} else { } else {
@ -417,6 +433,8 @@ export default class AppTile extends React.Component {
onFinished={this._closeContextMenu} onFinished={this._closeContextMenu}
showUnpin={!this.props.userWidget} showUnpin={!this.props.userWidget}
userWidget={this.props.userWidget} userWidget={this.props.userWidget}
onEditClick={this.props.onEditClick}
onDeleteClick={this.props.onDeleteClick}
/> />
); );
} }

View file

@ -155,12 +155,24 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
// show them in reverse, with latest pinned at the top // show them in reverse, with latest pinned at the top
content = pinnedEvents.filter(Boolean).reverse().map(ev => ( content = pinnedEvents.filter(Boolean).reverse().map(ev => (
<PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={onUnpinClicked} /> <PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={() => onUnpinClicked(ev)} />
)); ));
} else { } else {
content = <div className="mx_RightPanel_empty mx_PinnedMessagesCard_empty"> content = <div className="mx_PinnedMessagesCard_empty">
<h2>{_t("Youre all caught up")}</h2> <div>
<p>{_t("You have no visible notifications.")}</p> { /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
<div className="mx_PinnedMessagesCard_MessageActionBar">
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" />
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" />
</div>
<h2>{ _t("Nothing pinned, yet") }</h2>
{ _t("If you have permissions, open the menu on any message and select " +
"<b>Pin</b> to stick them here.", {}, {
b: sub => <b>{ sub }</b>,
}) }
</div>
</div>; </div>;
} }

View file

@ -25,7 +25,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import ReplyThread from "../elements/ReplyThread"; import ReplyThread from "../elements/ReplyThread";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as TextForEvent from "../../../TextForEvent"; import { hasText } from "../../../TextForEvent";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
@ -1210,7 +1210,7 @@ export function haveTileForEvent(e) {
const handler = getHandlerTile(e); const handler = getHandlerTile(e);
if (handler === undefined) return false; if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') { if (handler === 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== ''; return hasText(e);
} else if (handler === 'messages.RoomCreate') { } else if (handler === 'messages.RoomCreate') {
return Boolean(e.getContent()['predecessor']); return Boolean(e.getContent()['predecessor']);
} else { } else {

View file

@ -1,53 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 Michael Telatynski
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {Key} from '../../../Keyboard';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.ForwardMessage")
export default class ForwardMessage extends React.Component {
static propTypes = {
onCancelClick: PropTypes.func.isRequired,
};
componentDidMount() {
document.addEventListener('keydown', this._onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this._onKeyDown);
}
_onKeyDown = ev => {
switch (ev.key) {
case Key.ESCAPE:
this.props.onCancelClick();
break;
}
};
render() {
return (
<div className="mx_ForwardMessage">
<h1>{ _t('Please select the destination room for this message') }</h1>
</div>
);
}
}

View file

@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RateLimitedFunc from '../../../ratelimitedfunc'; import RateLimitedFunc from '../../../ratelimitedfunc';
import { CancelButton } from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore"; 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';
@ -42,7 +41,6 @@ export default class RoomHeader extends React.Component {
onSettingsClick: PropTypes.func, onSettingsClick: PropTypes.func,
onSearchClick: PropTypes.func, onSearchClick: PropTypes.func,
onLeaveClick: PropTypes.func, onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
e2eStatus: PropTypes.string, e2eStatus: PropTypes.string,
onAppsClick: PropTypes.func, onAppsClick: PropTypes.func,
appsShown: PropTypes.bool, appsShown: PropTypes.bool,
@ -52,7 +50,6 @@ export default class RoomHeader extends React.Component {
static defaultProps = { static defaultProps = {
editing: false, editing: false,
inRoom: false, inRoom: false,
onCancelClick: null,
}; };
componentDidMount() { componentDidMount() {
@ -83,11 +80,6 @@ export default class RoomHeader extends React.Component {
render() { render() {
let searchStatus = null; let searchStatus = null;
let cancelButton = null;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
// don't display the search count until the search completes and // don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount. // gives us a valid (possibly zero) searchCount.
@ -207,7 +199,6 @@ export default class RoomHeader extends React.Component {
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div> <div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
{ name } { name }
{ topicElement } { topicElement }
{ cancelButton }
{ rightRow } { rightRow }
<RoomHeaderButtons room={this.props.room} /> <RoomHeaderButtons room={this.props.room} />
</div> </div>

View file

@ -16,23 +16,9 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import AccessibleButton from '../elements/AccessibleButton';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
// cancel button which is shared between room header and simple room header
export function CancelButton(props) {
const {onClick} = props;
return (
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
<img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")} />
</AccessibleButton>
);
}
/* /*
* A stripped-down room header used for things like the user settings * A stripped-down room header used for things like the user settings
* and room directory. * and room directory.
@ -41,18 +27,13 @@ export function CancelButton(props) {
export default class SimpleRoomHeader extends React.Component { export default class SimpleRoomHeader extends React.Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
onCancelClick: PropTypes.func,
// `src` to a TintableSvg. Optional. // `src` to a TintableSvg. Optional.
icon: PropTypes.string, icon: PropTypes.string,
}; };
render() { render() {
let cancelButton;
let icon; let icon;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
if (this.props.icon) { if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg'); const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg icon = <TintableSvg
@ -66,7 +47,6 @@ export default class SimpleRoomHeader extends React.Component {
<div className="mx_RoomHeader_simpleHeader"> <div className="mx_RoomHeader_simpleHeader">
{ icon } { icon }
{ this.props.title } { this.props.title }
{ cancelButton }
</div> </div>
</div> </div>
); );

View file

@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent {
/** /**
* Launch the integration manager on the stickers integration page * Launch the integration manager on the stickers integration page
*/ */
_launchManageIntegrations() { _launchManageIntegrations = () => {
// TODO: Open the right integration manager for the widget // TODO: Open the right integration manager for the widget
if (SettingsStore.getValue("feature_many_integration_managers")) { if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll( IntegrationManagers.sharedInstance().openAll(
@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent {
this.state.widgetId, this.state.widgetId,
); );
} }
} };
render() { render() {
let stickerPicker; let stickerPicker;
@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent {
key="controls_hide_stickers" key="controls_hide_stickers"
className={className} className={className}
onClick={this._onHideStickersClick} onClick={this._onHideStickersClick}
active={this.state.showStickers} active={this.state.showStickers.toString()}
title={_t("Hide Stickers")} title={_t("Hide Stickers")}
> >
</AccessibleButton>; </AccessibleButton>;

View file

@ -31,7 +31,6 @@ const RoomContext = createContext<IState>({
canPeek: false, canPeek: false,
showApps: false, showApps: false,
isPeeking: false, isPeeking: false,
showReadReceipts: true,
showRightPanel: true, showRightPanel: true,
joining: false, joining: false,
atEndOfLiveTimeline: true, atEndOfLiveTimeline: true,
@ -41,6 +40,12 @@ const RoomContext = createContext<IState>({
canReact: false, canReact: false,
canReply: false, canReply: false,
layout: Layout.Group, layout: Layout.Group,
lowBandwidth: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: false, matrixClientIsReady: false,
dragCounter: 0, dragCounter: 0,
}); });

View file

@ -35,8 +35,8 @@ export const useSettingValue = <T>(settingName: string, roomId: string = null, e
}; };
// Hook to fetch whether a feature is enabled and dynamically update when that changes // Hook to fetch whether a feature is enabled and dynamically update when that changes
export const useFeatureEnabled = (featureName: string, roomId: string = null) => { export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => {
const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId)); const [enabled, setEnabled] = useState(SettingsStore.getValue<boolean>(featureName, roomId));
useEffect(() => { useEffect(() => {
const ref = SettingsStore.watchSetting(featureName, roomId, () => { const ref = SettingsStore.watchSetting(featureName, roomId, () => {

View file

@ -556,8 +556,8 @@
"%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.", "%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.",
"%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.", "%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.",
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).", "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
@ -1473,7 +1473,6 @@
"Encrypting your message...": "Encrypting your message...", "Encrypting your message...": "Encrypting your message...",
"Your message was sent": "Your message was sent", "Your message was sent": "Your message was sent",
"Failed to send": "Failed to send", "Failed to send": "Failed to send",
"Please select the destination room for this message": "Please select the destination room for this message",
"Scroll to most recent messages": "Scroll to most recent messages", "Scroll to most recent messages": "Scroll to most recent messages",
"Close preview": "Close preview", "Close preview": "Close preview",
"and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|other": "and %(count)s others...",
@ -1717,8 +1716,8 @@
"The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to", "The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to",
"Yours, or the other users internet connection": "Yours, or the other users internet connection", "Yours, or the other users internet connection": "Yours, or the other users internet connection",
"Yours, or the other users session": "Yours, or the other users session", "Yours, or the other users session": "Yours, or the other users session",
"Youre all caught up": "Youre all caught up", "Nothing pinned, yet": "Nothing pinned, yet",
"You have no visible notifications.": "You have no visible notifications.", "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
"Pinned messages": "Pinned messages", "Pinned messages": "Pinned messages",
"Room Info": "Room Info", "Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
@ -1926,6 +1925,8 @@
"Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widgets do not use message encryption.": "Widgets do not use message encryption.",
"Widget added by": "Widget added by", "Widget added by": "Widget added by",
"This widget may use cookies.": "This widget may use cookies.", "This widget may use cookies.": "This widget may use cookies.",
"Error loading Widget": "Error loading Widget",
"Error - Mixed content": "Error - Mixed content",
"Popout widget": "Popout widget", "Popout widget": "Popout widget",
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files", "Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
"Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages", "Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
@ -2204,6 +2205,13 @@
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.", "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Report a bug": "Report a bug", "Report a bug": "Report a bug",
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.", "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
"You don't have permission to do this": "You don't have permission to do this",
"Sending": "Sending",
"Sent": "Sent",
"Open link": "Open link",
"Forward message": "Forward message",
"Message preview": "Message preview",
"Search for rooms or people": "Search for rooms or people",
"Confirm abort of host creation": "Confirm abort of host creation", "Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
"Abort": "Abort", "Abort": "Abort",
@ -2628,6 +2636,8 @@
"Create a new community": "Create a new community", "Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
"Communities are changing to Spaces": "Communities are changing to Spaces", "Communities are changing to Spaces": "Communities are changing to Spaces",
"Youre all caught up": "Youre all caught up",
"You have no visible notifications.": "You have no visible notifications.",
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
"%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.", "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.",
"The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.",
@ -2662,7 +2672,6 @@
"Some of your messages have not been sent": "Some of your messages have not been sent", "Some of your messages have not been sent": "Some of your messages have not been sent",
"Delete all": "Delete all", "Delete all": "Delete all",
"Retry all": "Retry all", "Retry all": "Retry all",
"Sending": "Sending",
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete", "You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
@ -2723,6 +2732,8 @@
"A private space to organise your rooms": "A private space to organise your rooms", "A private space to organise your rooms": "A private space to organise your rooms",
"Me and my teammates": "Me and my teammates", "Me and my teammates": "Me and my teammates",
"A private space for you and your teammates": "A private space for you and your teammates", "A private space for you and your teammates": "A private space for you and your teammates",
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
"We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.",
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
"Inviting...": "Inviting...", "Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates", "Invite your teammates": "Invite your teammates",

View file

@ -63,8 +63,7 @@ export class WatchManager {
if (!inRoomId) { if (!inRoomId) {
// Fire updates to all the individual room watchers too, as they probably care about the change higher up. // Fire updates to all the individual room watchers too, as they probably care about the change higher up.
const callbacks = Array.from(roomWatchers.values()).flat(1); callbacks.push(...Array.from(roomWatchers.values()).flat(1));
callbacks.push(...callbacks);
} else if (roomWatchers.has(IRRELEVANT_ROOM)) { } else if (roomWatchers.has(IRRELEVANT_ROOM)) {
callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM)); callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM));
} }

View file

@ -17,6 +17,7 @@
import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import {IState} from "./components/structures/RoomView";
interface IDiff { interface IDiff {
isMemberEvent: boolean; isMemberEvent: boolean;
@ -47,11 +48,18 @@ function memberEventDiff(ev: MatrixEvent): IDiff {
return diff; return diff;
} }
export default function shouldHideEvent(ev: MatrixEvent): boolean { /**
// Wrap getValue() for readability. Calling the SettingsStore can be * Determines whether the given event should be hidden from timelines.
// fairly resource heavy, so the checks below should avoid hitting it * @param ev The event
// where possible. * @param ctx An optional RoomContext to pull cached settings values from to avoid
const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); * hitting the settings store
*/
export default function shouldHideEvent(ev: MatrixEvent, ctx?: IState): boolean {
// Accessing the settings store directly can be expensive if done frequently,
// so we should prefer using cached values if a RoomContext is available
const isEnabled = ctx ?
name => ctx[name] :
name => SettingsStore.getValue(name, ev.getRoomId());
// Hide redacted events // Hide redacted events
if (ev.isRedacted() && !isEnabled('showRedactions')) return true; if (ev.isRedacted() && !isEnabled('showRedactions')) return true;

View file

@ -54,8 +54,6 @@ const INITIAL_STATE = {
// Any error that has occurred during loading // Any error that has occurred during loading
roomLoadError: null, roomLoadError: null,
forwardingEvent: null,
quotingEvent: null, quotingEvent: null,
replyingToEvent: null, replyingToEvent: null,
@ -150,11 +148,6 @@ class RoomViewStore extends Store<ActionPayload> {
case 'on_logged_out': case 'on_logged_out':
this.reset(); this.reset();
break; break;
case 'forward_event':
this.setState({
forwardingEvent: payload.event,
});
break;
case 'reply_to_event': case 'reply_to_event':
// If currently viewed room does not match the room in which we wish to reply then change rooms // If currently viewed room does not match the room in which we wish to reply then change rooms
// this can happen when performing a search across all rooms // this can happen when performing a search across all rooms
@ -187,7 +180,6 @@ class RoomViewStore extends Store<ActionPayload> {
roomAlias: payload.room_alias, roomAlias: payload.room_alias,
initialEventId: payload.event_id, initialEventId: payload.event_id,
isInitialEventHighlighted: payload.highlighted, isInitialEventHighlighted: payload.highlighted,
forwardingEvent: null,
roomLoading: false, roomLoading: false,
roomLoadError: null, roomLoadError: null,
// should peek by default // should peek by default
@ -207,14 +199,6 @@ class RoomViewStore extends Store<ActionPayload> {
newState.replyingToEvent = payload.replyingToEvent; newState.replyingToEvent = payload.replyingToEvent;
} }
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'send_event',
room_id: newState.roomId,
event: this.state.forwardingEvent,
});
}
this.setState(newState); this.setState(newState);
if (payload.auto_join) { if (payload.auto_join) {
@ -428,11 +412,6 @@ class RoomViewStore extends Store<ActionPayload> {
return this.state.joinError; return this.state.joinError;
} }
// The mxEvent if one is about to be forwarded
public getForwardingEvent() {
return this.state.forwardingEvent;
}
// The mxEvent if one is currently being replied to/quoted // The mxEvent if one is currently being replied to/quoted
public getQuotingEvent() { public getQuotingEvent() {
return this.state.replyingToEvent; return this.state.replyingToEvent;

View file

@ -332,7 +332,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
} }
public getContainerWidgets(room: Room, container: Container): IApp[] { public getContainerWidgets(room: Room, container: Container): IApp[] {
return this.byRoom[room.roomId]?.[container]?.ordered || []; return this.byRoom[room?.roomId]?.[container]?.ordered || [];
} }
public isInContainer(room: Room, widget: IApp, container: Container): boolean { public isInContainer(room: Room, widget: IApp, container: Container): boolean {

View file

@ -19,11 +19,11 @@ limitations under the License.
* TODO: Convert this to a real TypeScript interface * TODO: Convert this to a real TypeScript interface
*/ */
export default class PermalinkConstructor { export default class PermalinkConstructor {
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string { forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string {
throw new Error("Not implemented"); throw new Error("Not implemented");
} }
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string { forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string {
throw new Error("Not implemented"); throw new Error("Not implemented");
} }
@ -73,12 +73,12 @@ export class PermalinkParts {
return new PermalinkParts(null, null, null, groupId, null); return new PermalinkParts(null, null, null, groupId, null);
} }
static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts { static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts {
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []); return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers);
} }
static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts { static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts {
return new PermalinkParts(roomId, eventId, null, null, viaServers || []); return new PermalinkParts(roomId, eventId, null, null, viaServers);
} }
get primaryEntityId(): string { get primaryEntityId(): string {

View file

@ -149,7 +149,7 @@ export class RoomPermalinkCreator {
// Prefer to use canonical alias for permalink if possible // Prefer to use canonical alias for permalink if possible
const alias = this.room.getCanonicalAlias(); const alias = this.room.getCanonicalAlias();
if (alias) { if (alias) {
return getPermalinkConstructor().forRoom(alias, this._serverCandidates); return getPermalinkConstructor().forRoom(alias);
} }
} }
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates); return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
@ -302,7 +302,7 @@ export function makeRoomPermalink(roomId: string): string {
} }
const permalinkCreator = new RoomPermalinkCreator(room); const permalinkCreator = new RoomPermalinkCreator(room);
permalinkCreator.load(); permalinkCreator.load();
return permalinkCreator.forRoom(); return permalinkCreator.forShareableRoom();
} }
export function makeGroupPermalink(groupId: string): string { export function makeGroupPermalink(groupId: string): string {

View file

@ -51,8 +51,20 @@ class WrappedMessagePanel extends React.Component {
}; };
render() { render() {
const roomContext = {
room,
roomId: room.roomId,
canReact: true,
canReply: true,
showReadReceipts: true,
showRedactions: false,
showJoinLeaves: false,
showAvatarChanges: false,
showDisplaynameChanges: true,
};
return <MatrixClientContext.Provider value={client}> return <MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={{ canReact: true, canReply: true, room, roomId: room.roomId }}> <RoomContext.Provider value={roomContext}>
<MessagePanel room={room} {...this.props} resizeNotifier={this.state.resizeNotifier} /> <MessagePanel room={room} {...this.props} resizeNotifier={this.state.resizeNotifier} />
</RoomContext.Provider> </RoomContext.Provider>
</MatrixClientContext.Provider>; </MatrixClientContext.Provider>;

View file

@ -0,0 +1,163 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
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 "../../../skinned-sdk";
import React from "react";
import {configure, mount} from "enzyme";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import {act} from "react-dom/test-utils";
import * as TestUtils from "../../../test-utils";
import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import {RoomPermalinkCreator} from "../../../../src/utils/permalinks/Permalinks";
import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog";
configure({ adapter: new Adapter() });
describe("ForwardDialog", () => {
const sourceRoom = "!111111111111111111:example.org";
const defaultMessage = TestUtils.mkMessage({
room: sourceRoom,
user: "@alice:example.org",
msg: "Hello world!",
event: true,
});
const defaultRooms = ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name));
const mountForwardDialog = async (message = defaultMessage, rooms = defaultRooms) => {
const client = MatrixClientPeg.get();
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
let wrapper;
await act(async () => {
wrapper = mount(
<ForwardDialog
matrixClient={client}
event={message}
permalinkCreator={new RoomPermalinkCreator(undefined, sourceRoom)}
onFinished={jest.fn()}
/>,
);
// Wait one tick for our profile data to load so the state update happens within act
await new Promise(resolve => setImmediate(resolve));
});
return wrapper;
};
beforeEach(() => {
TestUtils.stubClient();
DMRoomMap.makeShared();
MatrixClientPeg.get().getUserId = jest.fn().mockReturnValue("@bob:example.org");
});
it("shows a preview with us as the sender", async () => {
const wrapper = await mountForwardDialog();
const previewBody = wrapper.find(".mx_EventTile_body");
expect(previewBody.text()).toBe("Hello world!");
// We would just test SenderProfile for the user ID, but it's stubbed
const previewAvatar = wrapper.find(".mx_EventTile_avatar .mx_BaseAvatar_image");
expect(previewAvatar.prop("title")).toBe("@bob:example.org");
});
it("filters the rooms", async () => {
const wrapper = await mountForwardDialog();
expect(wrapper.find("Entry")).toHaveLength(3);
const searchInput = wrapper.find("SearchBox input");
searchInput.instance().value = "a";
searchInput.simulate("change");
expect(wrapper.find("Entry")).toHaveLength(2);
});
it("tracks message sending progress across multiple rooms", async () => {
const wrapper = await mountForwardDialog();
// Make sendEvent require manual resolution so we can see the sending state
let finishSend;
let cancelSend;
MatrixClientPeg.get().sendEvent = jest.fn(() => new Promise((resolve, reject) => {
finishSend = resolve;
cancelSend = reject;
}));
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
expect(firstButton.render().is(".mx_ForwardList_canSend")).toBe(true);
act(() => { firstButton.simulate("click"); });
expect(firstButton.render().is(".mx_ForwardList_sending")).toBe(true);
await act(async () => {
cancelSend();
// Wait one tick for the button to realize the send failed
await new Promise(resolve => setImmediate(resolve));
});
expect(firstButton.render().is(".mx_ForwardList_sendFailed")).toBe(true);
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1);
expect(secondButton.render().is(".mx_ForwardList_canSend")).toBe(true);
act(() => { secondButton.simulate("click"); });
expect(secondButton.render().is(".mx_ForwardList_sending")).toBe(true);
await act(async () => {
finishSend();
// Wait one tick for the button to realize the send succeeded
await new Promise(resolve => setImmediate(resolve));
});
expect(secondButton.render().is(".mx_ForwardList_sent")).toBe(true);
});
it("can render replies", async () => {
const replyMessage = TestUtils.mkEvent({
type: "m.room.message",
room: "!111111111111111111:example.org",
user: "@alice:example.org",
content: {
"msgtype": "m.text",
"body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
"m.relates_to": {
"m.in_reply_to": {
event_id: "$2222222222222222222222222222222222222222222",
},
},
},
event: true,
});
const wrapper = await mountForwardDialog(replyMessage);
expect(wrapper.find("ReplyThread")).toBeTruthy();
});
it("disables buttons for rooms without send permissions", async () => {
const readOnlyRoom = TestUtils.mkStubRoom("a", "a");
readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false);
const rooms = [readOnlyRoom, TestUtils.mkStubRoom("b", "b")];
const wrapper = await mountForwardDialog(undefined, rooms);
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
expect(firstButton.prop("disabled")).toBe(true);
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last();
expect(secondButton.prop("disabled")).toBe(false);
});
});

View file

@ -760,9 +760,9 @@ wrappy@1:
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@^6.1.0: ws@^6.1.0:
version "6.2.1" version "6.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
dependencies: dependencies:
async-limiter "~1.0.0" async-limiter "~1.0.0"

View file

@ -219,7 +219,7 @@ export function mkMessage(opts) {
return mkEvent(opts); return mkEvent(opts);
} }
export function mkStubRoom(roomId = null) { export function mkStubRoom(roomId = null, name) {
const stubTimeline = { getEvents: () => [] }; const stubTimeline = { getEvents: () => [] };
return { return {
roomId, roomId,
@ -238,6 +238,7 @@ export function mkStubRoom(roomId = null) {
getPendingEvents: () => [], getPendingEvents: () => [],
getLiveTimeline: () => stubTimeline, getLiveTimeline: () => stubTimeline,
getUnfilteredTimelineSet: () => null, getUnfilteredTimelineSet: () => null,
findEventById: () => null,
getAccountData: () => null, getAccountData: () => null,
hasMembershipState: () => null, hasMembershipState: () => null,
getVersion: () => '1', getVersion: () => '1',
@ -255,13 +256,17 @@ export function mkStubRoom(roomId = null) {
tags: {}, tags: {},
setBlacklistUnverifiedDevices: jest.fn(), setBlacklistUnverifiedDevices: jest.fn(),
on: jest.fn(), on: jest.fn(),
off: jest.fn(),
removeListener: jest.fn(), removeListener: jest.fn(),
getDMInviter: jest.fn(), getDMInviter: jest.fn(),
name,
getAvatarUrl: () => 'mxc://avatar.url/room.png', getAvatarUrl: () => 'mxc://avatar.url/room.png',
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png', getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
isSpaceRoom: jest.fn(() => false), isSpaceRoom: jest.fn(() => false),
getUnreadNotificationCount: jest.fn(() => 0), getUnreadNotificationCount: jest.fn(() => 0),
getEventReadUpTo: jest.fn(() => null), getEventReadUpTo: jest.fn(() => null),
getCanonicalAlias: jest.fn(),
getAltAliases: jest.fn().mockReturnValue([]),
timeline: [], timeline: [],
}; };
} }

View file

@ -34,7 +34,7 @@ function mockRoom(roomId, members, serverACL) {
return { return {
roomId, roomId,
getCanonicalAlias: () => roomId, getCanonicalAlias: () => null,
getJoinedMembers: () => members, getJoinedMembers: () => members,
getMember: (userId) => members.find(m => m.userId === userId), getMember: (userId) => members.find(m => m.userId === userId),
currentState: { currentState: {

View file

@ -2723,9 +2723,9 @@ css-select@^4.1.2:
nth-check "^2.0.0" nth-check "^2.0.0"
css-what@^5.0.0: css-what@^5.0.0:
version "5.0.0" version "5.0.1"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.0.tgz#f0bf4f8bac07582722346ab243f6a35b512cfc47" resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
integrity sha512-qxyKHQvgKwzwDWC/rGbT821eJalfupxYW2qbSJSAtdSTimsr/MlaGONoNLllaUPZWf8QnbcKM/kPVYUQuEKAFA== integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
cssesc@^3.0.0: cssesc@^3.0.0:
version "3.0.0" version "3.0.0"
@ -5717,8 +5717,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 "11.1.0" version "11.2.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
another-json "^0.2.0" another-json "^0.2.0"
@ -8049,9 +8049,9 @@ tree-kill@^1.2.2:
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
trim-newlines@^3.0.0: trim-newlines@^3.0.0:
version "3.0.0" version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
trough@^1.0.0: trough@^1.0.0:
version "1.0.5" version "1.0.5"