mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 09:46:09 +03:00
Merge pull request #5398 from matrix-org/t3chguy/fix/8141
Improvements around new room empty space interactions
This commit is contained in:
commit
0766519467
32 changed files with 644 additions and 413 deletions
|
@ -115,6 +115,7 @@
|
|||
@import "./views/elements/_InfoTooltip.scss";
|
||||
@import "./views/elements/_InlineSpinner.scss";
|
||||
@import "./views/elements/_ManageIntegsButton.scss";
|
||||
@import "./views/elements/_MiniAvatarUploader.scss";
|
||||
@import "./views/elements/_PowerSelector.scss";
|
||||
@import "./views/elements/_ProgressBar.scss";
|
||||
@import "./views/elements/_QRCode.scss";
|
||||
|
@ -139,6 +140,7 @@
|
|||
@import "./views/groups/_GroupUserSettings.scss";
|
||||
@import "./views/messages/_CreateEvent.scss";
|
||||
@import "./views/messages/_DateSeparator.scss";
|
||||
@import "./views/messages/_EventTileBubble.scss";
|
||||
@import "./views/messages/_MEmoteBody.scss";
|
||||
@import "./views/messages/_MFileBody.scss";
|
||||
@import "./views/messages/_MImageBody.scss";
|
||||
|
@ -182,6 +184,7 @@
|
|||
@import "./views/rooms/_MemberList.scss";
|
||||
@import "./views/rooms/_MessageComposer.scss";
|
||||
@import "./views/rooms/_MessageComposerFormatBar.scss";
|
||||
@import "./views/rooms/_NewRoomIntro.scss";
|
||||
@import "./views/rooms/_NotificationBadge.scss";
|
||||
@import "./views/rooms/_PinnedEventTile.scss";
|
||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||
|
|
|
@ -50,42 +50,8 @@ limitations under the License.
|
|||
color: $muted-fg-color;
|
||||
}
|
||||
|
||||
.mx_HomePage_userAvatar {
|
||||
position: relative;
|
||||
width: min-content;
|
||||
.mx_MiniAvatarUploader {
|
||||
margin: 0 auto;
|
||||
|
||||
&::before, &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
|
||||
right: -6px;
|
||||
bottom: -6px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: $primary-bg-color;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
background-color: $secondary-fg-color;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url('$(res)/img/element-icons/camera.svg');
|
||||
mask-size: 16px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.mx_HomePage_userAvatar_busy::after {
|
||||
background: url("$(res)/img/spinner.gif") no-repeat center;
|
||||
background-size: 80%;
|
||||
mask: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_HomePage_default_buttons {
|
||||
|
|
|
@ -153,16 +153,6 @@ limitations under the License.
|
|||
display: block;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_isAlone {
|
||||
height: 50px;
|
||||
line-height: $font-50px;
|
||||
|
||||
color: $primary-fg-color;
|
||||
opacity: 0.5;
|
||||
overflow-y: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_MatrixChat_useCompactLayout {
|
||||
.mx_RoomStatusBar {
|
||||
min-height: 40px;
|
||||
|
|
56
res/css/views/elements/_MiniAvatarUploader.scss
Normal file
56
res/css/views/elements/_MiniAvatarUploader.scss
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_MiniAvatarUploader {
|
||||
position: relative;
|
||||
width: min-content;
|
||||
|
||||
&::before, &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
|
||||
right: -6px;
|
||||
bottom: -6px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: $primary-bg-color;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
background-color: $secondary-fg-color;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url('$(res)/img/element-icons/camera.svg');
|
||||
mask-size: 16px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.mx_MiniAvatarUploader_busy::after {
|
||||
background: url("$(res)/img/spinner.gif") no-repeat center;
|
||||
background-size: 80%;
|
||||
mask: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MiniAvatarUploader_input {
|
||||
display: none;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,25 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_CreateEvent {
|
||||
background-color: $info-plinth-bg-color;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.mx_CreateEvent_image {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
width: 72px;
|
||||
height: 34px;
|
||||
|
||||
background-color: $primary-fg-color;
|
||||
mask: url('$(res)/img/room-continuation.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_CreateEvent_header {
|
||||
font-weight: bold;
|
||||
&::before {
|
||||
background-color: $composer-e2e-icon-color;
|
||||
mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
|
||||
}
|
||||
}
|
||||
|
|
60
res/css/views/messages/_EventTileBubble.scss
Normal file
60
res/css/views/messages/_EventTileBubble.scss
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_EventTileBubble {
|
||||
background-color: $dark-panel-bg-color;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
margin: 10px auto;
|
||||
max-width: 75%;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
||||
|
||||
&::before, &::after {
|
||||
position: relative;
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
content: "";
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mx_EventTileBubble_title, .mx_EventTileBubble_subtitle {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.mx_EventTileBubble_title {
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.mx_EventTileBubble_subtitle {
|
||||
font-size: $font-12px;
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
|
@ -15,41 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_MJitsiWidgetEvent {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
||||
|
||||
&::before {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
content: "";
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
background-color: $composer-e2e-icon-color; // XXX: Variable abuse
|
||||
margin-top: 4px;
|
||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||
}
|
||||
|
||||
.mx_MJitsiWidgetEvent_title {
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.mx_MJitsiWidgetEvent_subtitle {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.mx_MJitsiWidgetEvent_title,
|
||||
.mx_MJitsiWidgetEvent_subtitle {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,28 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_cryptoEvent {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
||||
|
||||
&.mx_cryptoEvent_icon::before,
|
||||
&.mx_cryptoEvent_icon::after {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
content: "";
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
background-color: $composer-e2e-icon-color;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
// white infill for the transparency
|
||||
&.mx_cryptoEvent_icon::before {
|
||||
background-color: #ffffff;
|
||||
|
@ -46,6 +24,11 @@ limitations under the License.
|
|||
mask-size: 90%;
|
||||
}
|
||||
|
||||
&.mx_cryptoEvent_icon::after {
|
||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
background-color: $composer-e2e-icon-color;
|
||||
}
|
||||
|
||||
&.mx_cryptoEvent_icon_verified::after {
|
||||
mask-image: url("$(res)/img/e2e/verified.svg");
|
||||
background-color: $accent-color;
|
||||
|
@ -56,25 +39,6 @@ limitations under the License.
|
|||
background-color: $notice-primary-color;
|
||||
}
|
||||
|
||||
.mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.mx_cryptoEvent_title {
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.mx_cryptoEvent_subtitle {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.mx_cryptoEvent_state, .mx_cryptoEvent_subtitle {
|
||||
font-size: $font-12px;
|
||||
}
|
||||
|
||||
.mx_cryptoEvent_state, .mx_cryptoEvent_buttons {
|
||||
grid-column: 3;
|
||||
|
@ -92,5 +56,7 @@ limitations under the License.
|
|||
margin: auto 0;
|
||||
text-align: center;
|
||||
color: $notice-secondary-color;
|
||||
overflow-wrap: break-word;
|
||||
font-size: $font-12px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,15 +25,6 @@ $left-gutter: 64px;
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.mx_EventTile_bubble {
|
||||
background-color: $dark-panel-bg-color;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin: 10px auto;
|
||||
max-width: 75%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mx_EventTile.mx_EventTile_info {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
@ -131,9 +122,10 @@ $left-gutter: 64px;
|
|||
grid-template-columns: 1fr 100px;
|
||||
|
||||
.mx_EventTile_line {
|
||||
margin-right: 0px;
|
||||
margin-right: 0;
|
||||
grid-column: 1 / 3;
|
||||
padding: 0;
|
||||
// override default padding of mx_EventTile_line so that we can be centered
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.mx_EventTile_msgOption {
|
||||
|
|
67
res/css/views/rooms/_NewRoomIntro.scss
Normal file
67
res/css/views/rooms/_NewRoomIntro.scss
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_NewRoomIntro {
|
||||
margin: 80px 0 48px 64px;
|
||||
|
||||
.mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) {
|
||||
&::before, &::after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.mx_NewRoomIntro_buttons {
|
||||
margin-top: 28px;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
line-height: $font-24px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
background-color: $button-fg-color;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_NewRoomIntro_inviteButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
}
|
||||
}
|
||||
|
||||
> h2 {
|
||||
margin-top: 24px;
|
||||
font-size: $font-24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
}
|
||||
}
|
11
res/img/element-icons/chat-bubbles.svg
Normal file
11
res/img/element-icons/chat-bubbles.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.90964 11.5297C9.09231 11.5297 11.6724 8.94865 11.6724 5.76483C11.6724 2.581 9.09231 0 5.90964 0C2.72697 0 0.146904 2.581 0.146904 5.76483C0.146904 6.65678 0.3494 7.50142 0.710912 8.25525L0.0648767 10.3556C-0.171716 11.1248 0.550948 11.8442 1.31906 11.6041L3.39724 10.9544C4.15657 11.323 5.00898 11.5297 5.90964 11.5297Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.03851 12.8449C5.70399 13.1151 6.4314 13.2638 7.19345 13.2638C10.3676 13.2638 13.5 10.6832 13.5 7.49979C13.5 6.63255 13.2676 5.81005 12.8651 5.07227C14.6487 6.05071 15.8583 7.94999 15.8583 10.1326C15.8583 11.0243 15.6564 11.8688 15.2959 12.6224L15.9404 14.7232C16.1765 15.4926 15.4533 16.2114 14.6854 15.9708L12.6155 15.322C11.8585 15.6902 11.0088 15.8966 10.111 15.8966C7.91459 15.8966 6.00594 14.661 5.03851 12.8449Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -1,6 +0,0 @@
|
|||
<svg width="72" height="34" viewBox="0 0 72 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 7.26087V1H28.7889V7.26087M1 7.26087V33H28.7889V7.26087M1 7.26087H28.7889M4.16583 4.13043H16.8291" stroke="#454545" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M43.2109 7.26087V1H70.9999V7.26087M43.2109 7.26087V33H70.9999V7.26087M43.2109 7.26087H70.9999M46.3768 4.13043H59.0401" stroke="#454545" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M27.03 28.8262C34.2226 28.8262 36.0207 26.343 36.0207 25.1014V16.0996C36.0207 12.1264 43.6283 11.3401 47.432 11.4436" stroke="black" stroke-width="2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 623 B |
|
@ -50,8 +50,8 @@ class Skinner {
|
|||
return null;
|
||||
}
|
||||
|
||||
// components have to be functions.
|
||||
const validType = typeof comp === 'function';
|
||||
// components have to be functions or forwardRef objects with a render function.
|
||||
const validType = typeof comp === 'function' || comp.render;
|
||||
if (!validType) {
|
||||
throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import {useContext, useRef, useState} from "react";
|
||||
import {useContext, useState} from "react";
|
||||
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
|
@ -24,16 +24,13 @@ import SdkConfig from "../../SdkConfig";
|
|||
import * as sdk from "../../index";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import {Transition} from "react-transition-group";
|
||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||
import {OwnProfileStore} from "../../stores/OwnProfileStore";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import Tooltip from "../views/elements/Tooltip";
|
||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import classNames from "classnames";
|
||||
import {ENTERING} from "react-transition-group/Transition";
|
||||
import MiniAvatarUploader, {AVATAR_SIZE} from "../views/elements/MiniAvatarUploader";
|
||||
|
||||
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
|
||||
const onClickExplore = () => dis.fire(Action.ViewRoomDirectory);
|
||||
|
@ -43,11 +40,9 @@ interface IProps {
|
|||
justRegistered?: boolean;
|
||||
}
|
||||
|
||||
const avatarSize = 52;
|
||||
|
||||
const getOwnProfile = (userId: string) => ({
|
||||
displayName: OwnProfileStore.instance.displayName || userId,
|
||||
avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(avatarSize),
|
||||
avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE),
|
||||
});
|
||||
|
||||
const UserWelcomeTop = () => {
|
||||
|
@ -57,56 +52,23 @@ const UserWelcomeTop = () => {
|
|||
useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => {
|
||||
setOwnProfile(getOwnProfile(userId));
|
||||
});
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const uploadRef = useRef<HTMLInputElement>();
|
||||
|
||||
return <div>
|
||||
<input
|
||||
type="file"
|
||||
ref={uploadRef}
|
||||
className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={async (ev) => {
|
||||
if (!ev.target.files?.length) return;
|
||||
setBusy(true);
|
||||
const file = ev.target.files[0];
|
||||
const uri = await cli.uploadContent(file);
|
||||
await cli.setAvatarUrl(uri);
|
||||
setBusy(false);
|
||||
}}
|
||||
accept="image/*"
|
||||
/>
|
||||
|
||||
<AccessibleButton
|
||||
className={classNames("mx_HomePage_userAvatar", {
|
||||
mx_HomePage_userAvatar_busy: busy,
|
||||
})}
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
uploadRef.current.click();
|
||||
}}
|
||||
<MiniAvatarUploader
|
||||
hasAvatar={!!ownProfile.avatarUrl}
|
||||
hasAvatarLabel={_t("Great, that'll help people know it's you")}
|
||||
noAvatarLabel={_t("Add a photo so people know it's you.")}
|
||||
setAvatarUrl={url => cli.setAvatarUrl(url)}
|
||||
>
|
||||
<BaseAvatar
|
||||
idName={userId}
|
||||
name={ownProfile.displayName}
|
||||
url={ownProfile.avatarUrl}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
width={AVATAR_SIZE}
|
||||
height={AVATAR_SIZE}
|
||||
resizeMethod="crop"
|
||||
/>
|
||||
|
||||
<Transition appear in timeout={3000}>
|
||||
{state => (
|
||||
<Tooltip
|
||||
label={ownProfile.avatarUrl || busy
|
||||
? _t("Great, that'll help people know it's you")
|
||||
: _t("Add a photo so people know it's you.")}
|
||||
visible={state !== ENTERING}
|
||||
forceOnRight
|
||||
/>
|
||||
)}
|
||||
</Transition>
|
||||
</AccessibleButton>
|
||||
</MiniAvatarUploader>
|
||||
|
||||
<h1>{ _t("Welcome %(name)s", { name: ownProfile.displayName }) }</h1>
|
||||
<h4>{ _t("Now, let's help you get started") }</h4>
|
||||
|
|
|
@ -30,6 +30,8 @@ import {_t} from "../../languageHandler";
|
|||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||
import DMRoomMap from "../../utils/DMRoomMap";
|
||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
@ -952,15 +954,25 @@ class CreationGrouper {
|
|||
}).reduce((a, b) => a.concat(b), []);
|
||||
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
||||
const ev = this.events[this.events.length - 1];
|
||||
|
||||
let summaryText;
|
||||
const roomId = ev.getRoomId();
|
||||
const creator = ev.sender ? ev.sender.name : ev.getSender();
|
||||
if (DMRoomMap.shared().getUserIdForRoomId(roomId)) {
|
||||
summaryText = _t("%(creator)s created this DM.", { creator });
|
||||
} else {
|
||||
summaryText = _t("%(creator)s created and configured the room.", { creator });
|
||||
}
|
||||
|
||||
ret.push(<NewRoomIntro key="newroomintro" />);
|
||||
|
||||
ret.push(
|
||||
<EventListSummary
|
||||
key="roomcreationsummary"
|
||||
events={this.events}
|
||||
onToggle={panel._onHeightChanged} // Update scroll state
|
||||
summaryMembers={[ev.sender]}
|
||||
summaryText={_t("%(creator)s created and configured the room.", {
|
||||
creator: ev.sender ? ev.sender.name : ev.getSender(),
|
||||
})}
|
||||
summaryText={summaryText}
|
||||
>
|
||||
{ eventTiles }
|
||||
</EventListSummary>,
|
||||
|
|
|
@ -41,9 +41,6 @@ export default class RoomStatusBar extends React.Component {
|
|||
static propTypes = {
|
||||
// the room this statusbar is representing.
|
||||
room: PropTypes.object.isRequired,
|
||||
// This is true when the user is alone in the room, but has also sent a message.
|
||||
// Used to suggest to the user to invite someone
|
||||
sentMessageAndIsAlone: PropTypes.bool,
|
||||
|
||||
// The active call in the room, if any (means we show the call bar
|
||||
// along with the status of the call)
|
||||
|
@ -68,10 +65,6 @@ export default class RoomStatusBar extends React.Component {
|
|||
// 'you are alone' bar
|
||||
onInviteClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'stop warning me' button in the
|
||||
// 'you are alone' bar
|
||||
onStopWarningClick: PropTypes.func,
|
||||
|
||||
// callback for when we do something that changes the size of the
|
||||
// status bar. This is used to trigger a re-layout in the parent
|
||||
// component.
|
||||
|
@ -159,10 +152,7 @@ export default class RoomStatusBar extends React.Component {
|
|||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
_getSize() {
|
||||
if (this._shouldShowConnectionError() ||
|
||||
this._showCallBar() ||
|
||||
this.props.sentMessageAndIsAlone
|
||||
) {
|
||||
if (this._shouldShowConnectionError() || this._showCallBar()) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (this.state.unsentMessages.length > 0) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
|
@ -325,24 +315,6 @@ export default class RoomStatusBar extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
// If you're alone in the room, and have sent a message, suggest to invite someone
|
||||
if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_isAlone">
|
||||
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
|
||||
"or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||
{},
|
||||
{
|
||||
'inviteText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
|
||||
'nowarnText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -150,7 +150,6 @@ export interface IState {
|
|||
guestsCanJoin: boolean;
|
||||
canPeek: boolean;
|
||||
showApps: boolean;
|
||||
isAlone: boolean;
|
||||
isPeeking: boolean;
|
||||
showingPinned: boolean;
|
||||
showReadReceipts: boolean;
|
||||
|
@ -223,7 +222,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
guestsCanJoin: false,
|
||||
canPeek: false,
|
||||
showApps: false,
|
||||
isAlone: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
|
@ -705,9 +703,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
private onAction = payload => {
|
||||
switch (payload.action) {
|
||||
case 'message_send_failed':
|
||||
case 'message_sent':
|
||||
this.checkIfAlone(this.state.room);
|
||||
this.checkDesktopNotifications();
|
||||
break;
|
||||
case 'post_sticker_message':
|
||||
this.injectSticker(
|
||||
|
@ -1025,36 +1022,15 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// rate limited because a power level change will emit an event for every member in the room.
|
||||
private updateRoomMembers = rateLimitedFunc((dueToMember) => {
|
||||
private updateRoomMembers = rateLimitedFunc(() => {
|
||||
this.updateDMState();
|
||||
|
||||
let memberCountInfluence = 0;
|
||||
if (dueToMember && dueToMember.membership === "invite" && this.state.room.getInvitedMemberCount() === 0) {
|
||||
// A member got invited, but the room hasn't detected that change yet. Influence the member
|
||||
// count by 1 to counteract this.
|
||||
memberCountInfluence = 1;
|
||||
}
|
||||
this.checkIfAlone(this.state.room, memberCountInfluence);
|
||||
|
||||
this.updateE2EStatus(this.state.room);
|
||||
}, 500);
|
||||
|
||||
private checkIfAlone(room: Room, countInfluence?: number) {
|
||||
let warnedAboutLonelyRoom = false;
|
||||
if (localStorage) {
|
||||
warnedAboutLonelyRoom = Boolean(localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId));
|
||||
}
|
||||
if (warnedAboutLonelyRoom) {
|
||||
if (this.state.isAlone) this.setState({isAlone: false});
|
||||
return;
|
||||
}
|
||||
|
||||
let joinedOrInvitedMemberCount = room.getJoinedMemberCount() + room.getInvitedMemberCount();
|
||||
if (countInfluence) joinedOrInvitedMemberCount += countInfluence;
|
||||
this.setState({isAlone: joinedOrInvitedMemberCount === 1});
|
||||
|
||||
// if they are not alone additionally prompt the user about notifications so they don't miss replies
|
||||
if (joinedOrInvitedMemberCount > 1 && Notifier.shouldShowPrompt()) {
|
||||
private checkDesktopNotifications() {
|
||||
const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
|
||||
// if they are not alone prompt the user about notifications so they don't miss replies
|
||||
if (memberCount > 1 && Notifier.shouldShowPrompt()) {
|
||||
showNotificationsToast(true);
|
||||
}
|
||||
}
|
||||
|
@ -1091,14 +1067,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
action: 'view_invite',
|
||||
roomId: this.state.room.roomId,
|
||||
});
|
||||
this.setState({isAlone: false}); // there's a good chance they'll invite someone
|
||||
};
|
||||
|
||||
private onStopAloneWarningClick = () => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, String(true));
|
||||
}
|
||||
this.setState({isAlone: false});
|
||||
};
|
||||
|
||||
private onJoinButtonClicked = () => {
|
||||
|
@ -1797,12 +1765,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||
statusBar = <RoomStatusBar
|
||||
room={this.state.room}
|
||||
sentMessageAndIsAlone={this.state.isAlone}
|
||||
callState={this.state.callState}
|
||||
callType={activeCall ? activeCall.type : null}
|
||||
isPeeking={myMembership !== "join"}
|
||||
onInviteClick={this.onInviteButtonClick}
|
||||
onStopWarningClick={this.onStopAloneWarningClick}
|
||||
onVisible={this.onStatusBarVisible}
|
||||
onHidden={this.onStatusBarHidden}
|
||||
/>;
|
||||
|
|
|
@ -35,6 +35,7 @@ interface IProps {
|
|||
height?: number;
|
||||
resizeMethod?: ResizeMethod;
|
||||
viewAvatarOnClick?: boolean;
|
||||
onClick?(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -130,7 +131,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
||||
const {room, oobData, viewAvatarOnClick, onClick, ...otherProps} = this.props;
|
||||
|
||||
const roomName = room ? room.name : oobData.name;
|
||||
|
||||
|
@ -139,7 +140,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls}
|
||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : null}
|
||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
90
src/components/views/elements/MiniAvatarUploader.tsx
Normal file
90
src/components/views/elements/MiniAvatarUploader.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useRef, useState} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip from './Tooltip';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useTimeout} from "../../../hooks/useTimeout";
|
||||
|
||||
export const AVATAR_SIZE = 52;
|
||||
|
||||
interface IProps {
|
||||
hasAvatar: boolean;
|
||||
noAvatarLabel?: string;
|
||||
hasAvatarLabel?: string;
|
||||
setAvatarUrl(url: string): Promise<void>;
|
||||
}
|
||||
|
||||
const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [hover, setHover] = useState(false);
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
useTimeout(() => {
|
||||
setShow(true);
|
||||
}, 3000); // show after 3 seconds
|
||||
useTimeout(() => {
|
||||
setShow(false);
|
||||
}, 13000); // hide after being shown for 10 seconds
|
||||
|
||||
const uploadRef = useRef<HTMLInputElement>();
|
||||
|
||||
const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel;
|
||||
|
||||
return <React.Fragment>
|
||||
<input
|
||||
type="file"
|
||||
ref={uploadRef}
|
||||
className="mx_MiniAvatarUploader_input"
|
||||
onChange={async (ev) => {
|
||||
if (!ev.target.files?.length) return;
|
||||
setBusy(true);
|
||||
const file = ev.target.files[0];
|
||||
const uri = await cli.uploadContent(file);
|
||||
await setAvatarUrl(uri);
|
||||
setBusy(false);
|
||||
}}
|
||||
accept="image/*"
|
||||
/>
|
||||
|
||||
<AccessibleButton
|
||||
className={classNames("mx_MiniAvatarUploader", {
|
||||
mx_MiniAvatarUploader_busy: busy,
|
||||
mx_MiniAvatarUploader_hasAvatar: hasAvatar,
|
||||
})}
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
uploadRef.current.click();
|
||||
}}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
{ children }
|
||||
|
||||
<Tooltip
|
||||
label={label}
|
||||
visible={!!label && (hover || show)}
|
||||
forceOnRight
|
||||
/>
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
export default MiniAvatarUploader;
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
|
||||
export default class EncryptionEvent extends React.Component {
|
||||
render() {
|
||||
const {mxEvent} = this.props;
|
||||
|
||||
let body;
|
||||
let classes = "mx_EventTile_bubble mx_cryptoEvent mx_cryptoEvent_icon";
|
||||
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId());
|
||||
if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) {
|
||||
body = <div>
|
||||
<div className="mx_cryptoEvent_title">{_t("Encryption enabled")}</div>
|
||||
<div className="mx_cryptoEvent_subtitle">
|
||||
{_t(
|
||||
"Messages in this room are end-to-end encrypted. " +
|
||||
"Learn more & verify this user in their user profile.",
|
||||
)}
|
||||
</div>
|
||||
</div>;
|
||||
} else if (isRoomEncrypted) {
|
||||
body = <div>
|
||||
<div className="mx_cryptoEvent_title">{_t("Encryption enabled")}</div>
|
||||
<div className="mx_cryptoEvent_subtitle">
|
||||
{_t("Ignored attempt to disable encryption")}
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
body = <div>
|
||||
<div className="mx_cryptoEvent_title">{_t("Encryption not enabled")}</div>
|
||||
<div className="mx_cryptoEvent_subtitle">{_t("The encryption used by this room isn't supported.")}</div>
|
||||
</div>;
|
||||
classes += " mx_cryptoEvent_icon_warning";
|
||||
}
|
||||
|
||||
return (<div className={classes}>
|
||||
{body}
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
EncryptionEvent.propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
68
src/components/views/messages/EncryptionEvent.tsx
Normal file
68
src/components/views/messages/EncryptionEvent.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {forwardRef, useContext} from 'react';
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({mxEvent}, ref) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const roomId = mxEvent.getRoomId();
|
||||
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
|
||||
|
||||
if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) {
|
||||
let subtitle: string;
|
||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (dmPartner) {
|
||||
const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner;
|
||||
subtitle = _t("Messages here are end-to-end encrypted. " +
|
||||
"Verify %(displayName)s in their profile - tap on their avatar.", { displayName });
|
||||
} else {
|
||||
subtitle = _t("Messages in this room are end-to-end encrypted. " +
|
||||
"When people join, you can verify them in their profile, just tap on their avatar.");
|
||||
}
|
||||
|
||||
return <EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon"
|
||||
title={_t("Encryption enabled")}
|
||||
subtitle={subtitle}
|
||||
/>;
|
||||
} else if (isRoomEncrypted) {
|
||||
return <EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon"
|
||||
title={_t("Encryption enabled")}
|
||||
subtitle={_t("Ignored attempt to disable encryption")}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon mx_cryptoEvent_icon_warning"
|
||||
title={_t("Encryption not enabled")}
|
||||
subtitle={_t("The encryption used by this room isn't supported.")}
|
||||
ref={ref}
|
||||
/>;
|
||||
});
|
||||
|
||||
export default EncryptionEvent;
|
34
src/components/views/messages/EventTileBubble.tsx
Normal file
34
src/components/views/messages/EventTileBubble.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {forwardRef, ReactNode} from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface IProps {
|
||||
className: string;
|
||||
title: string;
|
||||
subtitle?: ReactNode;
|
||||
}
|
||||
|
||||
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({ className, title, subtitle, children }, ref) => {
|
||||
return <div className={classNames("mx_EventTileBubble", className)} ref={ref}>
|
||||
<div className="mx_EventTileBubble_title">{ title }</div>
|
||||
{ subtitle && <div className="mx_EventTileBubble_subtitle">{ subtitle }</div> }
|
||||
{ children }
|
||||
</div>;
|
||||
});
|
||||
|
||||
export default EventTileBubble;
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -40,37 +41,24 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
|||
|
||||
if (!url) {
|
||||
// removed
|
||||
return (
|
||||
<div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
|
||||
<div className='mx_MJitsiWidgetEvent_title'>
|
||||
{_t('Video conference ended by %(senderName)s', {senderName})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <EventTileBubble
|
||||
className="mx_MJitsiWidgetEvent"
|
||||
title={_t('Video conference ended by %(senderName)s', {senderName})}
|
||||
/>;
|
||||
} else if (prevUrl) {
|
||||
// modified
|
||||
return (
|
||||
<div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
|
||||
<div className='mx_MJitsiWidgetEvent_title'>
|
||||
{_t('Video conference updated by %(senderName)s', {senderName})}
|
||||
</div>
|
||||
<div className='mx_MJitsiWidgetEvent_subtitle'>
|
||||
{joinCopy}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <EventTileBubble
|
||||
className="mx_MJitsiWidgetEvent"
|
||||
title={_t('Video conference updated by %(senderName)s', {senderName})}
|
||||
subtitle={joinCopy}
|
||||
/>;
|
||||
} else {
|
||||
// assume added
|
||||
return (
|
||||
<div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
|
||||
<div className='mx_MJitsiWidgetEvent_title'>
|
||||
{_t("Video conference started by %(senderName)s", {senderName})}
|
||||
</div>
|
||||
<div className='mx_MJitsiWidgetEvent_subtitle'>
|
||||
{joinCopy}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <EventTileBubble
|
||||
className="mx_MJitsiWidgetEvent"
|
||||
title={_t("Video conference started by %(senderName)s", {senderName})}
|
||||
subtitle={joinCopy}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import {getNameForEventRoom, userLabelForEventRoom}
|
||||
from '../../../utils/KeyVerificationStateObserver';
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
|
||||
export default class MKeyVerificationConclusion extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -115,14 +116,14 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
}
|
||||
|
||||
if (title) {
|
||||
const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId());
|
||||
const classes = classNames("mx_EventTile_bubble", "mx_cryptoEvent", "mx_cryptoEvent_icon", {
|
||||
const classes = classNames("mx_cryptoEvent mx_cryptoEvent_icon", {
|
||||
mx_cryptoEvent_icon_verified: request.done,
|
||||
});
|
||||
return (<div className={classes}>
|
||||
<div className="mx_cryptoEvent_title">{title}</div>
|
||||
<div className="mx_cryptoEvent_subtitle">{subtitle}</div>
|
||||
</div>);
|
||||
return <EventTileBubble
|
||||
className={classes}
|
||||
title={title}
|
||||
subtitle={userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId())}
|
||||
/>;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -24,6 +24,7 @@ import {getNameForEventRoom, userLabelForEventRoom}
|
|||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
|
||||
export default class MKeyVerificationRequest extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -146,10 +147,8 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
|
||||
if (!request.initiatedByMe) {
|
||||
const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId());
|
||||
title = (<div className="mx_cryptoEvent_title">{
|
||||
_t("%(name)s wants to verify", {name})}</div>);
|
||||
subtitle = (<div className="mx_cryptoEvent_subtitle">{
|
||||
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
|
||||
title = _t("%(name)s wants to verify", {name});
|
||||
subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId());
|
||||
if (request.canAccept) {
|
||||
stateNode = (<div className="mx_cryptoEvent_buttons">
|
||||
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
||||
|
@ -157,18 +156,18 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
</div>);
|
||||
}
|
||||
} else { // request sent by us
|
||||
title = (<div className="mx_cryptoEvent_title">{
|
||||
_t("You sent a verification request")}</div>);
|
||||
subtitle = (<div className="mx_cryptoEvent_subtitle">{
|
||||
userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId())}</div>);
|
||||
title = _t("You sent a verification request");
|
||||
subtitle = userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId());
|
||||
}
|
||||
|
||||
if (title) {
|
||||
return (<div className="mx_EventTile_bubble mx_cryptoEvent mx_cryptoEvent_icon">
|
||||
{title}
|
||||
{subtitle}
|
||||
{stateNode}
|
||||
</div>);
|
||||
return <EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon"
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
>
|
||||
{ stateNode }
|
||||
</EventTileBubble>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
|
||||
export default class RoomCreate extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -51,17 +52,16 @@ export default class RoomCreate extends React.Component {
|
|||
const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']);
|
||||
permalinkCreator.load();
|
||||
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
|
||||
return <div className="mx_CreateEvent">
|
||||
<div className="mx_CreateEvent_image" />
|
||||
<div className="mx_CreateEvent_header">
|
||||
{_t("This room is a continuation of another conversation.")}
|
||||
</div>
|
||||
<a className="mx_CreateEvent_link"
|
||||
href={predecessorPermalink}
|
||||
onClick={this._onLinkClicked}
|
||||
>
|
||||
const link = (
|
||||
<a href={predecessorPermalink} onClick={this._onLinkClicked}>
|
||||
{_t("Click here to see older messages.")}
|
||||
</a>
|
||||
</div>;
|
||||
);
|
||||
|
||||
return <EventTileBubble
|
||||
className="mx_CreateEvent"
|
||||
title={_t("This room is a continuation of another conversation.")}
|
||||
subtitle={link}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import ReplyThread from "../elements/ReplyThread";
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from "classnames";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import * as sdk from "../../../index";
|
||||
|
@ -646,12 +647,13 @@ export default class EventTile extends React.Component {
|
|||
|
||||
// Info messages are basically information about commands processed on a room
|
||||
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
|
||||
(eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) ||
|
||||
(eventType === "m.room.encryption") ||
|
||||
(eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
|
||||
(eventType === EventType.RoomCreate) ||
|
||||
(eventType === EventType.RoomEncryption) ||
|
||||
(tileHandler === "messages.MJitsiWidgetEvent");
|
||||
let isInfoMessage = (
|
||||
!isBubbleMessage && eventType !== 'm.room.message' &&
|
||||
eventType !== 'm.sticker' && eventType !== 'm.room.create'
|
||||
!isBubbleMessage && eventType !== EventType.RoomMessage &&
|
||||
eventType !== EventType.Sticker && eventType !== EventType.RoomCreate
|
||||
);
|
||||
|
||||
// If we're showing hidden events in the timeline, we should use the
|
||||
|
|
131
src/components/views/rooms/NewRoomIntro.tsx
Normal file
131
src/components/views/rooms/NewRoomIntro.tsx
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext} from "react";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MiniAvatarUploader, {AVATAR_SIZE} from "../elements/MiniAvatarUploader";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
const NewRoomIntro = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const {room, roomId} = useContext(RoomContext);
|
||||
|
||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
let body;
|
||||
if (dmPartner) {
|
||||
let caption;
|
||||
if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
|
||||
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
|
||||
}
|
||||
|
||||
const member = room?.getMember(dmPartner);
|
||||
const displayName = member?.rawDisplayName || dmPartner;
|
||||
body = <React.Fragment>
|
||||
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} onClick={() => {
|
||||
defaultDispatcher.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || {userId: dmPartner},
|
||||
});
|
||||
}} />
|
||||
|
||||
<h2>{ room.name }</h2>
|
||||
|
||||
<p>{_t("This is the beginning of your direct message history with <displayName/>.", {}, {
|
||||
displayName: () => <b>{ displayName }</b>,
|
||||
})}</p>
|
||||
{ caption && <p>{ caption }</p> }
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
|
||||
const canAddTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
|
||||
|
||||
const onTopicClick = () => {
|
||||
dis.dispatch({
|
||||
action: "open_room_settings",
|
||||
room_id: roomId,
|
||||
}, true);
|
||||
// focus the topic field to help the user find it as it'll gain an outline
|
||||
setImmediate(() => {
|
||||
window.document.getElementById("profileTopic").focus();
|
||||
});
|
||||
};
|
||||
|
||||
let topicText;
|
||||
if (canAddTopic && topic) {
|
||||
topicText = _t("Topic: %(topic)s (<a>edit</a>)", { topic }, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={onTopicClick}>{ sub }</AccessibleButton>,
|
||||
});
|
||||
} else if (topic) {
|
||||
topicText = _t("Topic: %(topic)s ", { topic });
|
||||
} else if (canAddTopic) {
|
||||
topicText = _t("<a>Add a topic</a> to help people know what it is about.", {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={onTopicClick}>{ sub }</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
|
||||
const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
|
||||
const creatorName = room?.getMember(creator)?.rawDisplayName || creator;
|
||||
|
||||
let createdText;
|
||||
if (creator === cli.getUserId()) {
|
||||
createdText = _t("You created this room.");
|
||||
} else {
|
||||
createdText = _t("%(displayName)s created this room.", {
|
||||
displayName: creatorName,
|
||||
});
|
||||
}
|
||||
|
||||
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||
body = <React.Fragment>
|
||||
<MiniAvatarUploader
|
||||
hasAvatar={!!avatarUrl}
|
||||
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
|
||||
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
|
||||
>
|
||||
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} />
|
||||
</MiniAvatarUploader>
|
||||
|
||||
<h2>{ room.name }</h2>
|
||||
|
||||
<p>{createdText} {_t("This is the start of <roomName/>.", {}, {
|
||||
roomName: () => <b>{ room.name }</b>,
|
||||
})}</p>
|
||||
<p>{topicText}</p>
|
||||
<div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary">
|
||||
{_t("Invite to this room")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_NewRoomIntro">
|
||||
{ body }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default NewRoomIntro;
|
|
@ -29,7 +29,6 @@ const RoomContext = createContext<IState>({
|
|||
guestsCanJoin: false,
|
||||
canPeek: false,
|
||||
showApps: false,
|
||||
isAlone: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
|
|
|
@ -1335,6 +1335,15 @@
|
|||
"Strikethrough": "Strikethrough",
|
||||
"Code block": "Code block",
|
||||
"Quote": "Quote",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "This is the beginning of your direct message history with <displayName/>.",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Topic: %(topic)s (<a>edit</a>)",
|
||||
"Topic: %(topic)s ": "Topic: %(topic)s ",
|
||||
"<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> to help people know what it is about.",
|
||||
"You created this room.": "You created this room.",
|
||||
"%(displayName)s created this room.": "%(displayName)s created this room.",
|
||||
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
|
||||
"This is the start of <roomName/>.": "This is the start of <roomName/>.",
|
||||
"No pinned messages.": "No pinned messages.",
|
||||
"Loading...": "Loading...",
|
||||
"Pinned Messages": "Pinned Messages",
|
||||
|
@ -1633,8 +1642,9 @@
|
|||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"View Source": "View Source",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
|
||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.",
|
||||
"Encryption enabled": "Encryption enabled",
|
||||
"Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.",
|
||||
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
|
||||
"Encryption not enabled": "Encryption not enabled",
|
||||
"The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.",
|
||||
|
@ -1680,8 +1690,8 @@
|
|||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
|
||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
|
||||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
|
||||
"This room is a continuation of another conversation.": "This room is a continuation of another conversation.",
|
||||
"Click here to see older messages.": "Click here to see older messages.",
|
||||
"This room is a continuation of another conversation.": "This room is a continuation of another conversation.",
|
||||
"Copied!": "Copied!",
|
||||
"Failed to copy": "Failed to copy",
|
||||
"Add an Integration": "Add an Integration",
|
||||
|
@ -2331,6 +2341,7 @@
|
|||
"Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.",
|
||||
"Self-verification request": "Self-verification request",
|
||||
"Logout": "Logout",
|
||||
"%(creator)s created this DM.": "%(creator)s created this DM.",
|
||||
"%(creator)s created and configured the room.": "%(creator)s created and configured the room.",
|
||||
"Your Communities": "Your Communities",
|
||||
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
|
||||
|
@ -2376,7 +2387,6 @@
|
|||
"Starting microphone...": "Starting microphone...",
|
||||
"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.",
|
||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||
"Search failed": "Search failed",
|
||||
|
|
|
@ -38,6 +38,7 @@ import { configure, mount } from "enzyme";
|
|||
import Velocity from 'velocity-animate';
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../src/contexts/RoomContext";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
|
@ -52,7 +53,7 @@ class WrappedMessagePanel extends React.Component {
|
|||
|
||||
render() {
|
||||
return <MatrixClientContext.Provider value={client}>
|
||||
<RoomContext.Provider value={{ canReact: true, canReply: true }}>
|
||||
<RoomContext.Provider value={{ canReact: true, canReply: true, room, roomId: room.roomId }}>
|
||||
<MessagePanel room={room} {...this.props} resizeNotifier={this.state.resizeNotifier} />
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>;
|
||||
|
@ -79,6 +80,8 @@ describe('MessagePanel', function() {
|
|||
// complete without this even if we mock the clock and tick it
|
||||
// what should be the correct amount of time).
|
||||
Velocity.mock = true;
|
||||
|
||||
DMRoomMap.makeShared();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
@ -433,8 +436,8 @@ describe('MessagePanel', function() {
|
|||
const rm = res.find('.mx_RoomView_myReadMarker_container').getDOMNode();
|
||||
|
||||
const rows = res.find('.mx_RoomView_MessageList').children();
|
||||
expect(rows.length).toEqual(6);
|
||||
expect(rm.previousSibling).toEqual(rows.at(4).getDOMNode());
|
||||
expect(rows.length).toEqual(7); // 6 events + the NewRoomIntro
|
||||
expect(rm.previousSibling).toEqual(rows.at(5).getDOMNode());
|
||||
|
||||
// read marker should be hidden given props and at the last event
|
||||
expect(isReadMarkerVisible(rm)).toBeFalsy();
|
||||
|
|
|
@ -242,6 +242,7 @@ export function mkStubRoom(roomId = null) {
|
|||
setBlacklistUnverifiedDevices: jest.fn(),
|
||||
on: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
getDMInviter: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue