mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 11:47:23 +03:00
Merge branch 'develop' into message-bubble-misc
This commit is contained in:
commit
35171ccd23
26 changed files with 165 additions and 93 deletions
|
@ -1,3 +1,10 @@
|
||||||
|
Changes in [3.29.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.29.1) (2021-09-13)
|
||||||
|
===================================================================================================
|
||||||
|
|
||||||
|
## 🔒 SECURITY FIXES
|
||||||
|
* Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing
|
||||||
|
for details.
|
||||||
|
|
||||||
Changes in [3.29.0](https://github.com/vector-im/element-desktop/releases/tag/v3.29.0) (2021-08-31)
|
Changes in [3.29.0](https://github.com/vector-im/element-desktop/releases/tag/v3.29.0) (2021-08-31)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.29.0",
|
"version": "3.29.1",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -139,7 +139,6 @@ $activeBorderColor: $secondary-content;
|
||||||
&:not(.mx_SpaceButton_narrow) {
|
&:not(.mx_SpaceButton_narrow) {
|
||||||
.mx_SpaceButton_selectionWrapper {
|
.mx_SpaceButton_selectionWrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-right: 16px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,7 +150,6 @@ $activeBorderColor: $secondary-content;
|
||||||
display: block;
|
display: block;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-right: 8px;
|
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
line-height: $font-18px;
|
line-height: $font-18px;
|
||||||
}
|
}
|
||||||
|
@ -225,8 +223,7 @@ $activeBorderColor: $secondary-content;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: relative;
|
||||||
right: 4px;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
@ -245,8 +242,6 @@ $activeBorderColor: $secondary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
// Create a flexbox to make aligning dot badges easier
|
// Create a flexbox to make aligning dot badges easier
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -264,6 +259,7 @@ $activeBorderColor: $secondary-content;
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
.mx_SpaceButton {
|
.mx_SpaceButton {
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
|
@ -293,19 +289,12 @@ $activeBorderColor: $secondary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.collapsed) {
|
&:not(.collapsed) {
|
||||||
.mx_SpacePanel_badgeContainer {
|
|
||||||
position: absolute;
|
|
||||||
right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SpaceButton:hover,
|
.mx_SpaceButton:hover,
|
||||||
.mx_SpaceButton:focus-within,
|
.mx_SpaceButton:focus-within,
|
||||||
.mx_SpaceButton_hasMenuOpen {
|
.mx_SpaceButton_hasMenuOpen {
|
||||||
&:not(.mx_SpaceButton_invite) {
|
&:not(.mx_SpaceButton_invite) {
|
||||||
// Hide the badge container on hover because it'll be a menu button
|
// Hide the badge container on hover because it'll be a menu button
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,14 +98,14 @@ limitations under the License.
|
||||||
transition:
|
transition:
|
||||||
font-size 0.25s ease-out 0.1s,
|
font-size 0.25s ease-out 0.1s,
|
||||||
color 0.25s ease-out 0.1s,
|
color 0.25s ease-out 0.1s,
|
||||||
top 0.25s ease-out 0.1s,
|
transform 0.25s ease-out 0.1s,
|
||||||
background-color 0.25s ease-out 0.1s;
|
background-color 0.25s ease-out 0.1s;
|
||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
|
transform: translateY(0);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
top: 0px;
|
|
||||||
margin: 7px 8px;
|
margin: 7px 8px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
pointer-events: none; // Allow clicks to fall through to the input
|
pointer-events: none; // Allow clicks to fall through to the input
|
||||||
|
@ -124,10 +124,10 @@ limitations under the License.
|
||||||
transition:
|
transition:
|
||||||
font-size 0.25s ease-out 0s,
|
font-size 0.25s ease-out 0s,
|
||||||
color 0.25s ease-out 0s,
|
color 0.25s ease-out 0s,
|
||||||
top 0.25s ease-out 0s,
|
transform 0.25s ease-out 0s,
|
||||||
background-color 0.25s ease-out 0s;
|
background-color 0.25s ease-out 0s;
|
||||||
font-size: $font-10px;
|
font-size: $font-10px;
|
||||||
top: -13px;
|
transform: translateY(-13px);
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
background-color: $field-focused-label-bg-color;
|
background-color: $field-focused-label-bg-color;
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
background: $background;
|
background: $background;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-radius: 8px 8px 0 0;
|
border-radius: 8px 8px 0 0;
|
||||||
max-height: 35vh;
|
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -64,6 +63,7 @@
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
max-height: 35vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Autocomplete_Completion_container_truncate {
|
.mx_Autocomplete_Completion_container_truncate {
|
||||||
|
|
|
@ -184,6 +184,9 @@ $visual-bell-bg-color: #800;
|
||||||
|
|
||||||
$room-warning-bg-color: $header-panel-bg-color;
|
$room-warning-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
|
$authpage-body-bg-color: $background;
|
||||||
|
$authpage-primary-color: $primary-content;
|
||||||
|
|
||||||
$dark-panel-bg-color: $header-panel-bg-color;
|
$dark-panel-bg-color: $header-panel-bg-color;
|
||||||
$panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1);
|
$panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1);
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,8 @@ $tab-label-fg-color: var(--timeline-text-color);
|
||||||
// was #4e5054
|
// was #4e5054
|
||||||
$authpage-lang-color: var(--timeline-text-color);
|
$authpage-lang-color: var(--timeline-text-color);
|
||||||
$roomheader-color: var(--timeline-text-color);
|
$roomheader-color: var(--timeline-text-color);
|
||||||
|
// was #232f32
|
||||||
|
$authpage-primary-color: var(--timeline-text-color);
|
||||||
// --roomlist-text-secondary-color
|
// --roomlist-text-secondary-color
|
||||||
$roomtile-preview-color: var(--roomlist-text-secondary-color);
|
$roomtile-preview-color: var(--roomlist-text-secondary-color);
|
||||||
$roomlist-header-color: var(--roomlist-text-secondary-color);
|
$roomlist-header-color: var(--roomlist-text-secondary-color);
|
||||||
|
|
|
@ -322,10 +322,16 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
const menuClasses = classNames({
|
const menuClasses = classNames({
|
||||||
'mx_ContextualMenu': true,
|
'mx_ContextualMenu': true,
|
||||||
'mx_ContextualMenu_left': !hasChevron && position.left,
|
/**
|
||||||
'mx_ContextualMenu_right': !hasChevron && position.right,
|
* In some cases we may get the number of 0, which still means that we're supposed to properly
|
||||||
'mx_ContextualMenu_top': !hasChevron && position.top,
|
* add the specific position class, but as it was falsy things didn't work as intended.
|
||||||
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
* In addition, defensively check for counter cases where we may get more than one value,
|
||||||
|
* even if we shouldn't.
|
||||||
|
*/
|
||||||
|
'mx_ContextualMenu_left': !hasChevron && position.left !== undefined && !position.right,
|
||||||
|
'mx_ContextualMenu_right': !hasChevron && position.right !== undefined && !position.left,
|
||||||
|
'mx_ContextualMenu_top': !hasChevron && position.top !== undefined && !position.bottom,
|
||||||
|
'mx_ContextualMenu_bottom': !hasChevron && position.bottom !== undefined && !position.top,
|
||||||
'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left,
|
'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left,
|
||||||
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
|
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
|
||||||
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
|
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
|
||||||
|
@ -404,17 +410,27 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ToRightOf = {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
chevronOffset: number;
|
||||||
|
};
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
||||||
export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">, chevronOffset = 12) => {
|
export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">, chevronOffset = 12): ToRightOf => {
|
||||||
const left = elementRect.right + window.pageXOffset + 3;
|
const left = elementRect.right + window.pageXOffset + 3;
|
||||||
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||||
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
||||||
return { left, top, chevronOffset };
|
return { left, top, chevronOffset };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AboveLeftOf = IPosition & {
|
||||||
|
chevronFace: ChevronFace;
|
||||||
|
};
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
||||||
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
||||||
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0): AboveLeftOf => {
|
||||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||||
|
|
||||||
const buttonRight = elementRect.right + window.pageXOffset;
|
const buttonRight = elementRect.right + window.pageXOffset;
|
||||||
|
|
|
@ -143,7 +143,7 @@ export enum Views {
|
||||||
SOFT_LOGOUT,
|
SOFT_LOGOUT,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas"];
|
const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];
|
||||||
|
|
||||||
// Actions that are redirected through the onboarding process prior to being
|
// Actions that are redirected through the onboarding process prior to being
|
||||||
// re-dispatched. NOTE: some actions are non-trivial and would require
|
// re-dispatched. NOTE: some actions are non-trivial and would require
|
||||||
|
|
|
@ -57,6 +57,7 @@ import { Key } from "../../Keyboard";
|
||||||
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
||||||
import { getDisplayAliasForRoom } from "./RoomDirectory";
|
import { getDisplayAliasForRoom } from "./RoomDirectory";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
import { useEventEmitterState } from "../../hooks/useEventEmitter";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -87,7 +88,8 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
|
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
|
||||||
const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0]
|
const joinedRoomName = useEventEmitterState(joinedRoom, "Room.name", room => room?.name);
|
||||||
|
const name = joinedRoomName || room.name || room.canonical_alias || room.aliases?.[0]
|
||||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||||
|
|
||||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||||
|
|
|
@ -78,6 +78,7 @@ import { CreateEventField, IGroupSummary } from "../views/dialogs/CreateSpaceFro
|
||||||
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import GroupAvatar from "../views/avatars/GroupAvatar";
|
import GroupAvatar from "../views/avatars/GroupAvatar";
|
||||||
|
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -191,6 +192,11 @@ interface ISpacePreviewProps {
|
||||||
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => {
|
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const myMembership = useMyRoomMembership(space);
|
const myMembership = useMyRoomMembership(space);
|
||||||
|
useDispatcher(defaultDispatcher, payload => {
|
||||||
|
if (payload.action === Action.JoinRoomError && payload.roomId === space.roomId) {
|
||||||
|
setBusy(false); // stop the spinner, join failed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
|
|
@ -826,7 +826,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
if (canAffectUser && me.powerLevel >= banPowerLevel) {
|
if (canAffectUser && me.powerLevel >= banPowerLevel) {
|
||||||
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||||
}
|
}
|
||||||
if (canAffectUser && me.powerLevel >= editPowerLevel) {
|
if (canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
|
||||||
muteButton = (
|
muteButton = (
|
||||||
<MuteToggleButton
|
<MuteToggleButton
|
||||||
member={member}
|
member={member}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
useContextMenu,
|
useContextMenu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
AboveLeftOf,
|
||||||
} from "../../structures/ContextMenu";
|
} from "../../structures/ContextMenu";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
|
@ -511,7 +512,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
null,
|
null,
|
||||||
];
|
];
|
||||||
|
|
||||||
let menuPosition;
|
let menuPosition: AboveLeftOf | undefined;
|
||||||
if (this.ref.current) {
|
if (this.ref.current) {
|
||||||
const contentRect = this.ref.current.getBoundingClientRect();
|
const contentRect = this.ref.current.getBoundingClientRect();
|
||||||
menuPosition = aboveLeftOf(contentRect);
|
menuPosition = aboveLeftOf(contentRect);
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { MouseEvent } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { formatCount } from "../../../utils/FormattingUtils";
|
import { formatCount } from "../../../utils/FormattingUtils";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
@ -22,6 +22,9 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { XOR } from "../../../@types/common";
|
import { XOR } from "../../../@types/common";
|
||||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Tooltip from "../elements/Tooltip";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
notification: NotificationState;
|
notification: NotificationState;
|
||||||
|
@ -39,6 +42,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
|
interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
|
||||||
|
showUnsentTooltip?: boolean;
|
||||||
/**
|
/**
|
||||||
* If specified will return an AccessibleButton instead of a div.
|
* If specified will return an AccessibleButton instead of a div.
|
||||||
*/
|
*/
|
||||||
|
@ -47,6 +51,7 @@ interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
|
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
|
||||||
|
showTooltip: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.NotificationBadge")
|
@replaceableComponent("views.rooms.NotificationBadge")
|
||||||
|
@ -59,6 +64,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
|
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
|
||||||
|
showTooltip: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.countWatcherRef = SettingsStore.watchSetting(
|
this.countWatcherRef = SettingsStore.watchSetting(
|
||||||
|
@ -93,9 +99,22 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
this.forceUpdate(); // notification state changed - update
|
this.forceUpdate(); // notification state changed - update
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onMouseOver = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({
|
||||||
|
showTooltip: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMouseLeave = () => {
|
||||||
|
this.setState({
|
||||||
|
showTooltip: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||||
const { notification, forceCount, roomId, onClick, ...props } = this.props;
|
const { notification, showUnsentTooltip, forceCount, roomId, onClick, ...props } = this.props;
|
||||||
|
|
||||||
// Don't show a badge if we don't need to
|
// Don't show a badge if we don't need to
|
||||||
if (notification.isIdle) return null;
|
if (notification.isIdle) return null;
|
||||||
|
@ -124,9 +143,24 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
|
let label: string;
|
||||||
|
let tooltip: JSX.Element;
|
||||||
|
if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
|
||||||
|
label = _t("Message didn't send. Click for info.");
|
||||||
|
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton {...props} className={classes} onClick={onClick}>
|
<AccessibleButton
|
||||||
|
aria-label={label}
|
||||||
|
{...props}
|
||||||
|
className={classes}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseOver={this.onMouseOver}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
>
|
||||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||||
|
{ tooltip }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -670,6 +670,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
onClick={this.onBadgeClick}
|
onClick={this.onBadgeClick}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
showUnsentTooltip={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { createRef } from "react";
|
import React, { createRef } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||||
|
@ -51,8 +50,6 @@ import IconizedContextMenu, {
|
||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { getUnsentMessages } from "../../structures/RoomStatusBar";
|
|
||||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -68,7 +65,6 @@ interface IState {
|
||||||
notificationsMenuPosition: PartialDOMRect;
|
notificationsMenuPosition: PartialDOMRect;
|
||||||
generalMenuPosition: PartialDOMRect;
|
generalMenuPosition: PartialDOMRect;
|
||||||
messagePreview?: string;
|
messagePreview?: string;
|
||||||
hasUnsentEvents: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
|
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
|
||||||
|
@ -95,7 +91,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||||
notificationsMenuPosition: null,
|
notificationsMenuPosition: null,
|
||||||
generalMenuPosition: null,
|
generalMenuPosition: null,
|
||||||
hasUnsentEvents: this.countUnsentEvents() > 0,
|
|
||||||
|
|
||||||
// generatePreview() will return nothing if the user has previews disabled
|
// generatePreview() will return nothing if the user has previews disabled
|
||||||
messagePreview: "",
|
messagePreview: "",
|
||||||
|
@ -106,11 +101,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||||
}
|
}
|
||||||
|
|
||||||
private countUnsentEvents(): number {
|
private onRoomNameUpdate = (room: Room) => {
|
||||||
return getUnsentMessages(this.props.room).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRoomNameUpdate = (room) => {
|
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -118,11 +109,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.forceUpdate(); // notification state changed - update
|
this.forceUpdate(); // notification state changed - update
|
||||||
};
|
};
|
||||||
|
|
||||||
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
|
||||||
if (room?.roomId !== this.props.room.roomId) return;
|
|
||||||
this.setState({ hasUnsentEvents: this.countUnsentEvents() > 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoomPropertyUpdate = (property: CachedRoomKey) => {
|
private onRoomPropertyUpdate = (property: CachedRoomKey) => {
|
||||||
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
|
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
|
||||||
// else ignore - not important for this tile
|
// else ignore - not important for this tile
|
||||||
|
@ -178,12 +164,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||||
this.roomProps.on("Room.name", this.onRoomNameUpdate);
|
this.props.room?.on("Room.name", this.onRoomNameUpdate);
|
||||||
CommunityPrototypeStore.instance.on(
|
CommunityPrototypeStore.instance.on(
|
||||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||||
this.onCommunityUpdate,
|
this.onCommunityUpdate,
|
||||||
);
|
);
|
||||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -208,7 +193,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||||
this.onCommunityUpdate,
|
this.onCommunityUpdate,
|
||||||
);
|
);
|
||||||
MatrixClientPeg.get()?.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
@ -587,20 +571,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
let badge: React.ReactNode;
|
let badge: React.ReactNode;
|
||||||
if (!this.props.isMinimized) {
|
if (!this.props.isMinimized && this.notificationState) {
|
||||||
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
||||||
if (this.state.hasUnsentEvents) {
|
|
||||||
// hardcode the badge to a danger state when there's unsent messages
|
|
||||||
badge = (
|
|
||||||
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
|
||||||
<NotificationBadge
|
|
||||||
notification={StaticNotificationState.RED_EXCLAMATION}
|
|
||||||
forceCount={false}
|
|
||||||
roomId={this.props.room.roomId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (this.notificationState) {
|
|
||||||
badge = (
|
badge = (
|
||||||
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
|
@ -611,7 +583,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let messagePreview = null;
|
let messagePreview = null;
|
||||||
if (this.showMessagePreview && this.state.messagePreview) {
|
if (this.showMessagePreview && this.state.messagePreview) {
|
||||||
|
|
|
@ -97,9 +97,8 @@ const spaceNameValidator = withValidation({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const nameToAlias = (name: string, domain: string): string => {
|
const nameToLocalpart = (name: string): string => {
|
||||||
const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
|
return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
|
||||||
return `#${localpart}:${domain}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// XXX: Temporary for the Spaces release only
|
// XXX: Temporary for the Spaces release only
|
||||||
|
@ -176,8 +175,9 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
||||||
value={name}
|
value={name}
|
||||||
onChange={ev => {
|
onChange={ev => {
|
||||||
const newName = ev.target.value;
|
const newName = ev.target.value;
|
||||||
if (!alias || alias === nameToAlias(name, domain)) {
|
if (!alias || alias === `#${nameToLocalpart(name)}:${domain}`) {
|
||||||
setAlias(nameToAlias(newName, domain));
|
setAlias(`#${nameToLocalpart(newName)}:${domain}`);
|
||||||
|
aliasFieldRef.current?.validate({ allowEmpty: true });
|
||||||
}
|
}
|
||||||
setName(newName);
|
setName(newName);
|
||||||
}}
|
}}
|
||||||
|
@ -194,7 +194,7 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
||||||
onChange={setAlias}
|
onChange={setAlias}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
value={alias}
|
value={alias}
|
||||||
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
|
placeholder={name ? nameToLocalpart(name) : _t("e.g. my-space")}
|
||||||
label={_t("Address")}
|
label={_t("Address")}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
@ -217,6 +217,7 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpaceCreateMenu = ({ onFinished }) => {
|
const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
const [visibility, setVisibility] = useState<Visibility>(null);
|
const [visibility, setVisibility] = useState<Visibility>(null);
|
||||||
const [busy, setBusy] = useState<boolean>(false);
|
const [busy, setBusy] = useState<boolean>(false);
|
||||||
|
|
||||||
|
@ -233,14 +234,18 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
|
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
// require & validate the space name field
|
// require & validate the space name field
|
||||||
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
|
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||||
spaceNameField.current.focus();
|
spaceNameField.current.focus();
|
||||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// validate the space name alias field but do not require it
|
|
||||||
if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
// validate the space alias field but do not require it
|
||||||
|
const aliasLocalpart = alias.substring(1, alias.length - cli.getDomain().length - 1);
|
||||||
|
if (visibility === Visibility.Public && aliasLocalpart &&
|
||||||
|
(await spaceAliasField.current.validate({ allowEmpty: true })) === false
|
||||||
|
) {
|
||||||
spaceAliasField.current.focus();
|
spaceAliasField.current.focus();
|
||||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
|
@ -248,7 +253,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createSpace(name, visibility === Visibility.Public, alias, topic, avatar);
|
await createSpace(
|
||||||
|
name,
|
||||||
|
visibility === Visibility.Public,
|
||||||
|
aliasLocalpart ? alias : undefined,
|
||||||
|
topic,
|
||||||
|
avatar,
|
||||||
|
);
|
||||||
|
|
||||||
onFinished();
|
onFinished();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -93,6 +93,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
notification={notificationState}
|
notification={notificationState}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
showUnsentTooltip={true}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,11 @@ import type { EventEmitter } from "events";
|
||||||
type Handler = (...args: any[]) => void;
|
type Handler = (...args: any[]) => void;
|
||||||
|
|
||||||
// Hook to wrap event emitter on and removeListener in hook lifecycle
|
// Hook to wrap event emitter on and removeListener in hook lifecycle
|
||||||
export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbol, handler: Handler) => {
|
export const useEventEmitter = (
|
||||||
|
emitter: EventEmitter | undefined,
|
||||||
|
eventName: string | symbol,
|
||||||
|
handler: Handler,
|
||||||
|
) => {
|
||||||
// Create a ref that stores handler
|
// Create a ref that stores handler
|
||||||
const savedHandler = useRef(handler);
|
const savedHandler = useRef(handler);
|
||||||
|
|
||||||
|
@ -51,7 +55,11 @@ export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbo
|
||||||
|
|
||||||
type Mapper<T> = (...args: any[]) => T;
|
type Mapper<T> = (...args: any[]) => T;
|
||||||
|
|
||||||
export const useEventEmitterState = <T>(emitter: EventEmitter, eventName: string | symbol, fn: Mapper<T>): T => {
|
export const useEventEmitterState = <T>(
|
||||||
|
emitter: EventEmitter | undefined,
|
||||||
|
eventName: string | symbol,
|
||||||
|
fn: Mapper<T>,
|
||||||
|
): T => {
|
||||||
const [value, setValue] = useState<T>(fn());
|
const [value, setValue] = useState<T>(fn());
|
||||||
const handler = useCallback((...args: any[]) => {
|
const handler = useCallback((...args: any[]) => {
|
||||||
setValue(fn(...args));
|
setValue(fn(...args));
|
||||||
|
|
|
@ -25,7 +25,7 @@ const defaultMapper: Mapper<RoomState> = (roomState: RoomState) => roomState;
|
||||||
|
|
||||||
// Hook to simplify watching Matrix Room state
|
// Hook to simplify watching Matrix Room state
|
||||||
export const useRoomState = <T extends any = RoomState>(
|
export const useRoomState = <T extends any = RoomState>(
|
||||||
room: Room,
|
room?: Room,
|
||||||
mapper: Mapper<T> = defaultMapper as Mapper<T>,
|
mapper: Mapper<T> = defaultMapper as Mapper<T>,
|
||||||
): T => {
|
): T => {
|
||||||
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
|
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
|
||||||
|
|
|
@ -1598,6 +1598,7 @@
|
||||||
"Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.",
|
"Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.",
|
||||||
"Enable encryption in settings.": "Enable encryption in settings.",
|
"Enable encryption in settings.": "Enable encryption in settings.",
|
||||||
"End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled",
|
"End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled",
|
||||||
|
"Message didn't send. Click for info.": "Message didn't send. Click for info.",
|
||||||
"Unpin": "Unpin",
|
"Unpin": "Unpin",
|
||||||
"View message": "View message",
|
"View message": "View message",
|
||||||
"%(duration)ss": "%(duration)ss",
|
"%(duration)ss": "%(duration)ss",
|
||||||
|
|
|
@ -629,11 +629,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => {
|
private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => {
|
||||||
const membership = newMembership || room.getMyMembership();
|
const roomMembership = room.getMyMembership();
|
||||||
|
if (!roomMembership) {
|
||||||
|
// room is still being baked in the js-sdk, we'll process it at Room.myMembership instead
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const membership = newMembership || roomMembership;
|
||||||
|
|
||||||
if (!room.isSpaceRoom()) {
|
if (!room.isSpaceRoom()) {
|
||||||
// this.onRoomUpdate(room);
|
// this.onRoomUpdate(room);
|
||||||
this.onRoomsUpdate();
|
// this.onRoomsUpdate();
|
||||||
|
// ideally we only need onRoomsUpdate here but it doesn't rebuild parentMap so always adds new rooms to Home
|
||||||
|
this.rebuild();
|
||||||
|
|
||||||
if (membership === "join") {
|
if (membership === "join") {
|
||||||
// the user just joined a room, remove it from the suggested list if it was there
|
// the user just joined a room, remove it from the suggested list if it was there
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class ListNotificationState extends NotificationState {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get symbol(): string {
|
public get symbol(): string {
|
||||||
return null; // This notification state doesn't support symbols
|
return this._color === NotificationColor.Unsent ? "!" : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setRooms(rooms: Room[]) {
|
public setRooms(rooms: Room[]) {
|
||||||
|
|
|
@ -21,4 +21,5 @@ export enum NotificationColor {
|
||||||
Bold, // no badge, show as unread
|
Bold, // no badge, show as unread
|
||||||
Grey, // unread notified messages
|
Grey, // unread notified messages
|
||||||
Red, // unread pings
|
Red, // unread pings
|
||||||
|
Unsent, // some messages failed to send
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import * as RoomNotifs from '../../RoomNotifs';
|
import * as RoomNotifs from '../../RoomNotifs';
|
||||||
import * as Unread from '../../Unread';
|
import * as Unread from '../../Unread';
|
||||||
import { NotificationState } from "./NotificationState";
|
import { NotificationState } from "./NotificationState";
|
||||||
|
import { getUnsentMessages } from "../../components/structures/RoomStatusBar";
|
||||||
|
|
||||||
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||||
constructor(public readonly room: Room) {
|
constructor(public readonly room: Room) {
|
||||||
|
@ -32,6 +33,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
this.room.on("Room.timeline", this.handleRoomEventUpdate);
|
this.room.on("Room.timeline", this.handleRoomEventUpdate);
|
||||||
this.room.on("Room.redaction", this.handleRoomEventUpdate);
|
this.room.on("Room.redaction", this.handleRoomEventUpdate);
|
||||||
this.room.on("Room.myMembership", this.handleMembershipUpdate);
|
this.room.on("Room.myMembership", this.handleMembershipUpdate);
|
||||||
|
this.room.on("Room.localEchoUpdated", this.handleLocalEchoUpdated);
|
||||||
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
|
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
|
||||||
MatrixClientPeg.get().on("accountData", this.handleAccountDataUpdate);
|
MatrixClientPeg.get().on("accountData", this.handleAccountDataUpdate);
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
|
@ -47,12 +49,17 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
||||||
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
||||||
this.room.removeListener("Room.myMembership", this.handleMembershipUpdate);
|
this.room.removeListener("Room.myMembership", this.handleMembershipUpdate);
|
||||||
|
this.room.removeListener("Room.localEchoUpdated", this.handleLocalEchoUpdated);
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
|
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.handleAccountDataUpdate);
|
MatrixClientPeg.get().removeListener("accountData", this.handleAccountDataUpdate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleLocalEchoUpdated = () => {
|
||||||
|
this.updateNotificationState();
|
||||||
|
};
|
||||||
|
|
||||||
private handleReadReceipt = (event: MatrixEvent, room: Room) => {
|
private handleReadReceipt = (event: MatrixEvent, room: Room) => {
|
||||||
if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore
|
if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore
|
||||||
if (room.roomId !== this.room.roomId) return; // not for us - ignore
|
if (room.roomId !== this.room.roomId) return; // not for us - ignore
|
||||||
|
@ -79,7 +86,12 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
private updateNotificationState() {
|
private updateNotificationState() {
|
||||||
const snapshot = this.snapshot();
|
const snapshot = this.snapshot();
|
||||||
|
|
||||||
if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
|
if (getUnsentMessages(this.room).length > 0) {
|
||||||
|
// When there are unsent messages we show a red `!`
|
||||||
|
this._color = NotificationColor.Unsent;
|
||||||
|
this._symbol = "!";
|
||||||
|
this._count = 1; // not used, technically
|
||||||
|
} else if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
|
||||||
// When muted we suppress all notification states, even if we have context on them.
|
// When muted we suppress all notification states, even if we have context on them.
|
||||||
this._color = NotificationColor.None;
|
this._color = NotificationColor.None;
|
||||||
this._symbol = null;
|
this._symbol = null;
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class SpaceNotificationState extends NotificationState {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get symbol(): string {
|
public get symbol(): string {
|
||||||
return null; // This notification state doesn't support symbols
|
return this._color === NotificationColor.Unsent ? "!" : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setRooms(rooms: Room[]) {
|
public setRooms(rooms: Room[]) {
|
||||||
|
@ -54,7 +54,7 @@ export class SpaceNotificationState extends NotificationState {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFirstRoomWithNotifications() {
|
public getFirstRoomWithNotifications() {
|
||||||
return this.rooms.find((room) => room.getUnreadNotificationCount() > 0).roomId;
|
return Object.values(this.states).find(state => state.color >= this.color)?.room.roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
|
@ -83,4 +83,3 @@ export class SpaceNotificationState extends NotificationState {
|
||||||
this.emitIfUpdated(snapshot);
|
this.emitIfUpdated(snapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue