mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/types/react
This commit is contained in:
commit
42d33cf091
@ -19,49 +19,68 @@ limitations under the License.
|
||||
padding: 24px;
|
||||
background-color: $settings-profile-placeholder-bg-color;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
|
||||
> div {
|
||||
.mx_BetaCard_title {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-18px;
|
||||
line-height: $font-22px;
|
||||
color: $primary-fg-color;
|
||||
margin: 4px 0 14px;
|
||||
.mx_BetaCard_columns {
|
||||
display: flex;
|
||||
|
||||
.mx_BetaCard_betaPill {
|
||||
margin-left: 12px;
|
||||
> div {
|
||||
.mx_BetaCard_title {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-18px;
|
||||
line-height: $font-22px;
|
||||
color: $primary-fg-color;
|
||||
margin: 4px 0 14px;
|
||||
|
||||
.mx_BetaCard_betaPill {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_BetaCard_caption {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-20px;
|
||||
color: $secondary-fg-color;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mx_BetaCard_buttons .mx_AccessibleButton {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
padding: 7px 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.mx_BetaCard_disclaimer {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_BetaCard_caption {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-20px;
|
||||
color: $secondary-fg-color;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
padding: 7px 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.mx_BetaCard_disclaimer {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
margin-top: 20px;
|
||||
> img {
|
||||
margin: auto 0 auto 20px;
|
||||
width: 300px;
|
||||
object-fit: contain;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> img {
|
||||
margin: auto 0 auto 20px;
|
||||
width: 300px;
|
||||
object-fit: contain;
|
||||
height: 100%;
|
||||
.mx_BetaCard_relatedSettings {
|
||||
.mx_SettingsFlag {
|
||||
margin: 16px 0 0;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $primary-fg-color;
|
||||
|
||||
.mx_SettingsFlag_microcopy {
|
||||
margin-top: 4px;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $secondary-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
13
src/@types/global.d.ts
vendored
13
src/@types/global.d.ts
vendored
@ -113,19 +113,6 @@ declare global {
|
||||
usageDetails?: {[key: string]: number};
|
||||
}
|
||||
|
||||
export interface ISettledFulfilled<T> {
|
||||
status: "fulfilled";
|
||||
value: T;
|
||||
}
|
||||
export interface ISettledRejected {
|
||||
status: "rejected";
|
||||
reason: any;
|
||||
}
|
||||
|
||||
interface PromiseConstructor {
|
||||
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
type?: string;
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
|
@ -17,13 +17,12 @@ limitations under the License.
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
export type ResizeMethod = "crop" | "scale";
|
||||
|
||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||
export function avatarUrlForMember(
|
||||
member: RoomMember,
|
||||
|
@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import * as React from 'react';
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
@ -1019,9 +1019,8 @@ export const Commands = [
|
||||
const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
|
||||
dis.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the
|
||||
// receiver wants.
|
||||
member: member || {userId},
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || { userId } as User,
|
||||
});
|
||||
return success();
|
||||
},
|
||||
|
@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {ReactElement} from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import { ReactElement } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
import CommandProvider from './CommandProvider';
|
||||
import CommunityProvider from './CommunityProvider';
|
||||
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||
@ -24,7 +25,7 @@ import RoomProvider from './RoomProvider';
|
||||
import UserProvider from './UserProvider';
|
||||
import EmojiProvider from './EmojiProvider';
|
||||
import NotifProvider from './NotifProvider';
|
||||
import {timeout} from "../utils/promise";
|
||||
import { timeout } from "../utils/promise";
|
||||
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import SpaceProvider from "./SpaceProvider";
|
||||
|
@ -15,7 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import { _t } from '../languageHandler';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
|
@ -17,16 +17,16 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {uniqBy, sortBy} from "lodash";
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import { uniqBy, sortBy } from "lodash";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import {PillCompletion} from './Components';
|
||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||
import { PillCompletion } from './Components';
|
||||
import { makeRoomPermalink } from "../utils/permalinks/Permalinks";
|
||||
import { ICompletion, ISelectionRange } from "./Autocompleter";
|
||||
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
|
@ -23,7 +23,7 @@ limitations under the License.
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
@ -172,11 +172,7 @@ export interface IState {
|
||||
// We load this later by asking the js-sdk to suggest a version for us.
|
||||
// This object is the result of Room#getRecommendedVersion()
|
||||
|
||||
upgradeRecommendation?: {
|
||||
version: string;
|
||||
needsUpgrade: boolean;
|
||||
urgent: boolean;
|
||||
};
|
||||
upgradeRecommendation?: IRecommendedVersion;
|
||||
canReact: boolean;
|
||||
canReply: boolean;
|
||||
layout: Layout;
|
||||
@ -2058,7 +2054,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
|
||||
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
||||
jumpToBottom = (<JumpToBottomButton
|
||||
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
||||
highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
|
||||
numUnreadMessages={this.state.numUnreadMessages}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
roomId={this.state.roomId}
|
||||
|
@ -17,16 +17,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { toPx } from "../../../utils/units";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
interface IProps {
|
||||
|
@ -15,10 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
export interface IProps {
|
||||
groupId?: string;
|
||||
|
@ -16,14 +16,14 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
||||
member: RoomMember;
|
||||
|
@ -13,17 +13,17 @@ 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, {ComponentProps} from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import ImageView from '../elements/ImageView';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||
// Room may be left unset here, but if it is,
|
||||
|
@ -25,6 +25,7 @@ import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import Modal from "../../../Modal";
|
||||
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import SettingsFlag from "../elements/SettingsFlag";
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
@ -66,7 +67,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
|
||||
const info = SettingsStore.getBetaInfo(featureId);
|
||||
if (!info) return null; // Beta is invalid/disabled
|
||||
|
||||
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info;
|
||||
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info;
|
||||
const value = SettingsStore.getValue(featureId);
|
||||
|
||||
let feedbackButton;
|
||||
@ -82,26 +83,33 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
|
||||
}
|
||||
|
||||
return <div className="mx_BetaCard">
|
||||
<div>
|
||||
<h3 className="mx_BetaCard_title">
|
||||
{ titleOverride || _t(title) }
|
||||
<BetaPill />
|
||||
</h3>
|
||||
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
|
||||
<div className="mx_BetaCard_columns">
|
||||
<div>
|
||||
{ feedbackButton }
|
||||
<AccessibleButton
|
||||
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
|
||||
kind={feedbackButton ? "primary_outline" : "primary"}
|
||||
>
|
||||
{ value ? _t("Leave the beta") : _t("Join the beta") }
|
||||
</AccessibleButton>
|
||||
<h3 className="mx_BetaCard_title">
|
||||
{ titleOverride || _t(title) }
|
||||
<BetaPill />
|
||||
</h3>
|
||||
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
|
||||
<div className="mx_BetaCard_buttons">
|
||||
{ feedbackButton }
|
||||
<AccessibleButton
|
||||
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
|
||||
kind={feedbackButton ? "primary_outline" : "primary"}
|
||||
>
|
||||
{ value ? _t("Leave the beta") : _t("Join the beta") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{ disclaimer && <div className="mx_BetaCard_disclaimer">
|
||||
{ disclaimer(value) }
|
||||
</div> }
|
||||
</div>
|
||||
{ disclaimer && <div className="mx_BetaCard_disclaimer">
|
||||
{ disclaimer(value) }
|
||||
</div> }
|
||||
<img src={image} alt="" />
|
||||
</div>
|
||||
<img src={image} alt="" />
|
||||
{ extraSettings && <div className="mx_BetaCard_relatedSettings">
|
||||
{ extraSettings.map(key => (
|
||||
<SettingsFlag key={key} name={key} level={SettingLevel.DEVICE} />
|
||||
)) }
|
||||
</div> }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -44,7 +44,12 @@ const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
|
||||
const sendFeedback = async (ok: boolean) => {
|
||||
if (!ok) return onFinished(false);
|
||||
|
||||
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact);
|
||||
const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => {
|
||||
o[k] = SettingsStore.getValue(k);
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData);
|
||||
onFinished(true);
|
||||
|
||||
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
|
||||
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => {
|
||||
}, [room]);
|
||||
|
||||
if (children) return children(name);
|
||||
return name || "";
|
||||
return <>{ name || "" }</>;
|
||||
};
|
||||
|
||||
export default RoomName;
|
||||
|
@ -77,9 +77,10 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||
public render() {
|
||||
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
|
||||
|
||||
let label = this.props.label;
|
||||
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
else label = _t(label);
|
||||
const label = this.props.label
|
||||
? _t(this.props.label)
|
||||
: SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
const description = SettingsStore.getDescription(this.props.name);
|
||||
|
||||
if (this.props.useCheckbox) {
|
||||
return <StyledCheckbox
|
||||
@ -99,6 +100,9 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||
disabled={this.props.disabled || !canChange}
|
||||
aria-label={label}
|
||||
/>
|
||||
{ description && <div className="mx_SettingsFlag_microcopy">
|
||||
{ description }
|
||||
</div> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -14,28 +14,29 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { PHASE_REQUESTED, PHASE_UNSENT } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
import EncryptionInfo from "./EncryptionInfo";
|
||||
import VerificationPanel from "./VerificationPanel";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {ensureDMExists} from "../../../createRoom";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { ensureDMExists } from "../../../createRoom";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import Modal from "../../../Modal";
|
||||
import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import * as sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
|
||||
// cancellation codes which constitute a key mismatch
|
||||
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
|
||||
|
||||
interface IProps {
|
||||
member: RoomMember;
|
||||
member: RoomMember | User;
|
||||
onClose: () => void;
|
||||
verificationRequest: VerificationRequest;
|
||||
verificationRequestPromise: Promise<VerificationRequest>;
|
||||
|
@ -1594,7 +1594,7 @@ const UserInfo: React.FC<IProps> = ({
|
||||
content = (
|
||||
<BasicUserInfo
|
||||
room={room}
|
||||
member={member}
|
||||
member={member as User}
|
||||
groupId={groupId as string}
|
||||
devices={devices}
|
||||
isRoomEncrypted={isRoomEncrypted} />
|
||||
@ -1605,7 +1605,7 @@ const UserInfo: React.FC<IProps> = ({
|
||||
content = (
|
||||
<EncryptionPanel
|
||||
{...props as React.ComponentProps<typeof EncryptionPanel>}
|
||||
member={member}
|
||||
member={member as User | RoomMember}
|
||||
onClose={onEncryptionPanelClose}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
/>
|
||||
|
@ -15,10 +15,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room'
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import classNames from 'classnames';
|
||||
import { lexicographicCompare } from 'matrix-js-sdk/src/utils';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room'
|
||||
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
@ -106,9 +108,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
||||
|
||||
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
|
||||
const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
|
||||
stateEvs.sort((a, b) => {
|
||||
return a.getStateKey() < b.getStateKey();
|
||||
});
|
||||
stateEvs.sort((a, b) => lexicographicCompare(a.getStateKey(), b.getStateKey()));
|
||||
|
||||
for (const ev of stateEvs) {
|
||||
const title = ev.getContent().title;
|
||||
|
@ -14,30 +14,31 @@ 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 React, { useContext } from "react";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MiniAvatarUploader, {AVATAR_SIZE} from "../elements/MiniAvatarUploader";
|
||||
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 { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import {showSpaceInvite} from "../../../utils/space";
|
||||
|
||||
import { showSpaceInvite } from "../../../utils/space";
|
||||
import { privateShouldBeEncrypted } from "../../../createRoom";
|
||||
|
||||
import EventTileBubble from "../messages/EventTileBubble";
|
||||
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
|
||||
|
||||
function hasExpectedEncryptionSettings(room): boolean {
|
||||
const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId);
|
||||
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
||||
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
||||
const isPublic: boolean = room.getJoinRule() === "public";
|
||||
return isPublic || !privateShouldBeEncrypted() || isEncrypted;
|
||||
}
|
||||
@ -61,7 +62,7 @@ const NewRoomIntro = () => {
|
||||
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},
|
||||
member: member || { userId: dmPartner } as User,
|
||||
});
|
||||
}} />
|
||||
|
||||
@ -194,7 +195,7 @@ const NewRoomIntro = () => {
|
||||
|
||||
return <div className="mx_NewRoomIntro">
|
||||
|
||||
{ !hasExpectedEncryptionSettings(room) && (
|
||||
{ !hasExpectedEncryptionSettings(cli, room) && (
|
||||
<EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||
title={_t("End-to-end encryption isn't enabled")}
|
||||
|
@ -119,7 +119,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
};
|
||||
|
||||
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
||||
if (!room?.roomId === this.props.room.roomId) return;
|
||||
if (room?.roomId !== this.props.room.roomId) return;
|
||||
this.setState({hasUnsentEvents: this.countUnsentEvents() > 0});
|
||||
};
|
||||
|
||||
@ -316,7 +316,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
0,
|
||||
));
|
||||
} else {
|
||||
console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
|
||||
console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.roomId}`);
|
||||
}
|
||||
|
||||
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
|
@ -26,6 +26,7 @@ import {SpaceItem} from "./SpaceTreeLevel";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore, {
|
||||
HOME_SPACE,
|
||||
UPDATE_INVITED_SPACES,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
UPDATE_TOP_LEVEL_SPACES,
|
||||
@ -40,6 +41,7 @@ import {
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import {NotificationState} from "../../../stores/notifications/NotificationState";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IButtonProps {
|
||||
space?: Room;
|
||||
@ -205,6 +207,10 @@ const SpacePanel = () => {
|
||||
|
||||
const activeSpaces = activeSpace ? [activeSpace] : [];
|
||||
const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
|
||||
|
||||
const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
|
||||
? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
|
||||
|
||||
// TODO drag and drop for re-arranging order
|
||||
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
{({onKeyDownHandler}) => (
|
||||
@ -218,8 +224,8 @@ const SpacePanel = () => {
|
||||
className="mx_SpaceButton_home"
|
||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||
selected={!activeSpace}
|
||||
tooltip={_t("All rooms")}
|
||||
notificationState={RoomNotificationStateStore.instance.globalState}
|
||||
tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
|
||||
notificationState={homeNotificationState}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
{ invites.map(s => <SpaceItem
|
||||
|
@ -14,10 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from "../MatrixClientPeg";
|
||||
import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent";
|
||||
import {ResizeMethod} from "../Avatar";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { IMediaEventContent, IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
|
||||
|
||||
// Populate this class with the details of your customisations when copying it.
|
||||
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { ActionPayload } from "../payloads";
|
||||
import { Action } from "../actions";
|
||||
|
||||
@ -25,5 +26,5 @@ export interface ViewUserPayload extends ActionPayload {
|
||||
* The member to view. May be null or falsy to indicate that no member
|
||||
* should be shown (hide whichever relevant components).
|
||||
*/
|
||||
member?: RoomMember;
|
||||
member?: RoomMember | User;
|
||||
}
|
||||
|
@ -794,6 +794,10 @@
|
||||
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
|
||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
|
||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
|
||||
"Show all rooms in Home": "Show all rooms in Home",
|
||||
"Show people in spaces": "Show people in spaces",
|
||||
"If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.",
|
||||
"Show notification badges for People in Spaces": "Show notification badges for People in Spaces",
|
||||
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
|
||||
"Send and receive voice messages": "Send and receive voice messages",
|
||||
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
||||
@ -1024,6 +1028,7 @@
|
||||
"Expand space panel": "Expand space panel",
|
||||
"Collapse space panel": "Collapse space panel",
|
||||
"All rooms": "All rooms",
|
||||
"Home": "Home",
|
||||
"Click to copy": "Click to copy",
|
||||
"Copied!": "Copied!",
|
||||
"Failed to copy": "Failed to copy",
|
||||
@ -2031,7 +2036,6 @@
|
||||
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||
"And %(count)s more...|other": "And %(count)s more...",
|
||||
"Home": "Home",
|
||||
"Enter a server name": "Enter a server name",
|
||||
"Looks good": "Looks good",
|
||||
"You are not allowed to view this server's rooms list": "You are not allowed to view this server's rooms list",
|
||||
|
@ -92,7 +92,7 @@ export class BanList {
|
||||
if (!room) return;
|
||||
|
||||
for (const eventType of ALL_RULE_TYPES) {
|
||||
const events = room.currentState.getStateEvents(eventType, undefined);
|
||||
const events = room.currentState.getStateEvents(eventType);
|
||||
for (const ev of events) {
|
||||
if (!ev.getStateKey()) continue;
|
||||
|
||||
|
@ -263,7 +263,13 @@ function uint8ToString(buf: Buffer) {
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function submitFeedback(endpoint: string, label: string, comment: string, canContact = false) {
|
||||
export async function submitFeedback(
|
||||
endpoint: string,
|
||||
label: string,
|
||||
comment: string,
|
||||
canContact = false,
|
||||
extraData: Record<string, string> = {},
|
||||
) {
|
||||
let version = "UNKNOWN";
|
||||
try {
|
||||
version = await PlatformPeg.get().getAppVersion();
|
||||
@ -279,6 +285,10 @@ export async function submitFeedback(endpoint: string, label: string, comment: s
|
||||
body.append("platform", PlatformPeg.get().getHumanReadableName());
|
||||
body.append("user_id", MatrixClientPeg.get()?.getUserId());
|
||||
|
||||
for (const k in extraData) {
|
||||
body.append(k, extraData[k]);
|
||||
}
|
||||
|
||||
await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2018, 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -94,6 +94,9 @@ export interface ISetting {
|
||||
[level: SettingLevel]: string;
|
||||
};
|
||||
|
||||
// Optional description which will be shown as microCopy under SettingsFlags
|
||||
description?: string;
|
||||
|
||||
// The supported levels are required. Preferably, use the preset arrays
|
||||
// at the top of this file to define this rather than a custom array.
|
||||
supportedLevels?: SettingLevel[];
|
||||
@ -127,6 +130,7 @@ export interface ISetting {
|
||||
image: string; // require(...)
|
||||
feedbackSubheading?: string;
|
||||
feedbackLabel?: string;
|
||||
extraSettings?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@ -174,8 +178,33 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||
feedbackSubheading: _td("Your feedback will help make spaces better. " +
|
||||
"The more detail you can go into, the better."),
|
||||
feedbackLabel: "spaces-feedback",
|
||||
extraSettings: [
|
||||
"feature_spaces.all_rooms",
|
||||
"feature_spaces.space_member_dms",
|
||||
"feature_spaces.space_dm_badges",
|
||||
],
|
||||
},
|
||||
},
|
||||
"feature_spaces.all_rooms": {
|
||||
displayName: _td("Show all rooms in Home"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: true,
|
||||
controller: new ReloadOnChangeController(),
|
||||
},
|
||||
"feature_spaces.space_member_dms": {
|
||||
displayName: _td("Show people in spaces"),
|
||||
description: _td("If disabled, you can still add Direct Messages to Personal Spaces. " +
|
||||
"If enabled, you'll automatically see everyone who is a member of the Space."),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: true,
|
||||
controller: new ReloadOnChangeController(),
|
||||
},
|
||||
"feature_spaces.space_dm_badges": {
|
||||
displayName: _td("Show notification badges for People in Spaces"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
controller: new ReloadOnChangeController(),
|
||||
},
|
||||
"feature_dnd": {
|
||||
isFeature: true,
|
||||
displayName: _td("Show options to enable 'Do not disturb' mode"),
|
||||
|
@ -248,6 +248,16 @@ export default class SettingsStore {
|
||||
return _t(displayName as string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the translated description for a given setting
|
||||
* @param {string} settingName The setting to look up.
|
||||
* @return {String} The description for the setting, or null if not found.
|
||||
*/
|
||||
public static getDescription(settingName: string) {
|
||||
if (!SETTINGS[settingName]?.description) return null;
|
||||
return _t(SETTINGS[settingName].description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a setting is also a feature.
|
||||
* @param {string} settingName The setting to look up.
|
||||
|
@ -107,8 +107,9 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
||||
|
||||
const pl = generalChat.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!pl) return this.isAdminOf(communityId);
|
||||
const plContent = pl.getContent();
|
||||
|
||||
const invitePl = isNullOrUndefined(pl.invite) ? 50 : Number(pl.invite);
|
||||
const invitePl = isNullOrUndefined(plContent.invite) ? 50 : Number(plContent.invite);
|
||||
return invitePl <= myMember.powerLevel;
|
||||
}
|
||||
|
||||
@ -159,10 +160,16 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
||||
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId);
|
||||
if (data && data.getContent()) {
|
||||
return {displayName: data.getContent().name, avatarMxc: data.getContent().avatar_url};
|
||||
return {
|
||||
displayName: data.getContent().name,
|
||||
avatarMxc: data.getContent().avatar_url,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {displayName: room.name, avatarMxc: room.avatar_url};
|
||||
return {
|
||||
displayName: room.name,
|
||||
avatarMxc: room.getMxcAvatarUrl(),
|
||||
};
|
||||
}
|
||||
|
||||
protected async onReady(): Promise<any> {
|
||||
|
@ -276,7 +276,7 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||
const address = this.state.roomAlias || this.state.roomId;
|
||||
const viaServers = this.state.viaServers || [];
|
||||
try {
|
||||
await retry<void, MatrixError>(() => cli.joinRoom(address, {
|
||||
await retry<any, MatrixError>(() => cli.joinRoom(address, {
|
||||
viaServers,
|
||||
...payload.opts,
|
||||
}), NUM_JOIN_RETRY, (err) => {
|
||||
|
@ -14,37 +14,42 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {ListIteratee, Many, sortBy, throttle} from "lodash";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import { ListIteratee, Many, sortBy, throttle } from "lodash";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import {AsyncStoreWithClient} from "./AsyncStoreWithClient";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import {ActionPayload} from "../dispatcher/payloads";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import RoomListStore from "./room-list/RoomListStore";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import DMRoomMap from "../utils/DMRoomMap";
|
||||
import {FetchRoomFn} from "./notifications/ListNotificationState";
|
||||
import {SpaceNotificationState} from "./notifications/SpaceNotificationState";
|
||||
import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
|
||||
import {DefaultTagID} from "./room-list/models";
|
||||
import {EnhancedMap, mapDiff} from "../utils/maps";
|
||||
import {setHasDiff} from "../utils/sets";
|
||||
import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
|
||||
import { FetchRoomFn } from "./notifications/ListNotificationState";
|
||||
import { SpaceNotificationState } from "./notifications/SpaceNotificationState";
|
||||
import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore";
|
||||
import { DefaultTagID } from "./room-list/models";
|
||||
import { EnhancedMap, mapDiff } from "../utils/maps";
|
||||
import { setHasDiff } from "../utils/sets";
|
||||
import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "../components/structures/SpaceRoomDirectory";
|
||||
import RoomViewStore from "./RoomViewStore";
|
||||
import {Action} from "../dispatcher/actions";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { arrayHasDiff } from "../utils/arrays";
|
||||
import { objectDiff } from "../utils/objects";
|
||||
|
||||
type SpaceKey = string | symbol;
|
||||
|
||||
interface IState {}
|
||||
|
||||
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
||||
|
||||
export const HOME_SPACE = Symbol("home-space");
|
||||
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
|
||||
|
||||
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
||||
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
|
||||
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
||||
// Space Room ID will be emitted when a Space's children change
|
||||
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
|
||||
|
||||
export interface ISuggestedRoom extends ISpaceSummaryRoom {
|
||||
viaServers: string[];
|
||||
@ -52,7 +57,8 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom {
|
||||
|
||||
const MAX_SUGGESTED_ROOMS = 20;
|
||||
|
||||
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`;
|
||||
const homeSpaceKey = SettingsStore.getValue("feature_spaces.all_rooms") ? "ALL_ROOMS" : "HOME_SPACE";
|
||||
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`;
|
||||
|
||||
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
|
||||
return arr.reduce((result, room: Room) => {
|
||||
@ -86,13 +92,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
|
||||
// The spaces representing the roots of the various tree-like hierarchies
|
||||
private rootSpaces: Room[] = [];
|
||||
// The list of rooms not present in any currently joined spaces
|
||||
private orphanedRooms = new Set<string>();
|
||||
// Map from room ID to set of spaces which list it as a child
|
||||
private parentMap = new EnhancedMap<string, Set<string>>();
|
||||
// Map from spaceId to SpaceNotificationState instance representing that space
|
||||
private notificationStateMap = new Map<string, SpaceNotificationState>();
|
||||
// Map from SpaceKey to SpaceNotificationState instance representing that space
|
||||
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
|
||||
// Map from space key to Set of room IDs that should be shown as part of that space's filter
|
||||
private spaceFilteredRooms = new Map<string, Set<string>>();
|
||||
// The space currently selected in the Space Panel - if null then All Rooms is selected
|
||||
private spaceFilteredRooms = new Map<SpaceKey, Set<string>>();
|
||||
// The space currently selected in the Space Panel - if null then Home is selected
|
||||
private _activeSpace?: Room = null;
|
||||
private _suggestedRooms: ISuggestedRoom[] = [];
|
||||
private _invitedSpaces = new Set<Room>();
|
||||
@ -134,7 +142,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
// if the space being selected is an invite then always view that invite
|
||||
// else if the last viewed room in this space is joined then view that
|
||||
// else view space home or home depending on what is being clicked on
|
||||
if (space?.getMyMembership !== "invite" &&
|
||||
if (space?.getMyMembership() !== "invite" &&
|
||||
this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join"
|
||||
) {
|
||||
defaultDispatcher.dispatch({
|
||||
@ -252,10 +260,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
}
|
||||
|
||||
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
||||
if (!space) {
|
||||
if (!space && SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
|
||||
}
|
||||
return this.spaceFilteredRooms.get(space.roomId) || new Set();
|
||||
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
|
||||
};
|
||||
|
||||
private rebuild = throttle(() => {
|
||||
@ -286,7 +294,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
});
|
||||
});
|
||||
|
||||
const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren));
|
||||
const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
|
||||
|
||||
// somewhat algorithm to handle full-cycles
|
||||
const detachedNodes = new Set<Room>(spaces);
|
||||
@ -327,6 +335,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
// rootSpaces.push(space);
|
||||
// });
|
||||
|
||||
this.orphanedRooms = new Set(orphanedRooms);
|
||||
this.rootSpaces = rootSpaces;
|
||||
this.parentMap = backrefs;
|
||||
|
||||
@ -343,10 +352,30 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
||||
}, 100, {trailing: true, leading: true});
|
||||
|
||||
onSpaceUpdate = () => {
|
||||
private onSpaceUpdate = () => {
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
private showInHomeSpace = (room: Room) => {
|
||||
if (SettingsStore.getValue("feature_spaces.all_rooms")) return true;
|
||||
if (room.isSpaceRoom()) return false;
|
||||
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|
||||
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
|
||||
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
|
||||
};
|
||||
|
||||
// Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
|
||||
// This can only change whether it shows up in the HOME_SPACE or not
|
||||
private onRoomUpdate = (room: Room) => {
|
||||
if (this.showInHomeSpace(room)) {
|
||||
this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId);
|
||||
this.emit(HOME_SPACE);
|
||||
} else if (!this.orphanedRooms.has(room.roomId)) {
|
||||
this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId);
|
||||
this.emit(HOME_SPACE);
|
||||
}
|
||||
};
|
||||
|
||||
private onSpaceMembersChange = (ev: MatrixEvent) => {
|
||||
// skip this update if we do not have a DM with this user
|
||||
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
|
||||
@ -360,6 +389,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
const oldFilteredRooms = this.spaceFilteredRooms;
|
||||
this.spaceFilteredRooms = new Map();
|
||||
|
||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
// put all room invites in the Home Space
|
||||
const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
|
||||
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
|
||||
|
||||
visibleRooms.forEach(room => {
|
||||
if (this.showInHomeSpace(room)) {
|
||||
this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.rootSpaces.forEach(s => {
|
||||
// traverse each space tree in DFS to build up the supersets as you go up,
|
||||
// reusing results from like subtrees.
|
||||
@ -375,13 +416,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
const roomIds = new Set(childRooms.map(r => r.roomId));
|
||||
const space = this.matrixClient?.getRoom(spaceId);
|
||||
|
||||
// Add relevant DMs
|
||||
space?.getMembers().forEach(member => {
|
||||
if (member.membership !== "join" && member.membership !== "invite") return;
|
||||
DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
|
||||
roomIds.add(roomId);
|
||||
if (SettingsStore.getValue("feature_spaces.space_member_dms")) {
|
||||
// Add relevant DMs
|
||||
space?.getMembers().forEach(member => {
|
||||
if (member.membership !== "join" && member.membership !== "invite") return;
|
||||
DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
|
||||
roomIds.add(roomId);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const newPath = new Set(parentPath).add(spaceId);
|
||||
childSpaces.forEach(childSpace => {
|
||||
@ -407,6 +450,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
// Update NotificationStates
|
||||
this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => {
|
||||
if (roomIds.has(room.roomId)) {
|
||||
if (s !== HOME_SPACE && SettingsStore.getValue("feature_spaces.space_dm_badges")) return true;
|
||||
|
||||
return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|
||||
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
|
||||
}
|
||||
@ -424,8 +469,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId));
|
||||
}
|
||||
if (!parent) {
|
||||
const parents = Array.from(this.parentMap.get(roomId) || []);
|
||||
parent = parents.find(p => this.matrixClient.getRoom(p));
|
||||
const parentIds = Array.from(this.parentMap.get(roomId) || []);
|
||||
for (const parentId of parentIds) {
|
||||
const room = this.matrixClient.getRoom(parentId);
|
||||
if (room) {
|
||||
parent = room;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// don't trigger a context switch when we are switching a space to match the chosen room
|
||||
@ -490,6 +541,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
// TODO confirm this after implementing parenting behaviour
|
||||
if (room.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
} else if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
this.emit(room.roomId);
|
||||
break;
|
||||
@ -502,8 +555,38 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => {
|
||||
if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) {
|
||||
// If the room was in favourites and now isn't or the opposite then update its position in the trees
|
||||
const oldTags = lastEvent?.getContent()?.tags || {};
|
||||
const newTags = ev.getContent()?.tags || {};
|
||||
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
|
||||
if (ev.getType() === EventType.Direct) {
|
||||
const lastContent = lastEvent.getContent();
|
||||
const content = ev.getContent();
|
||||
|
||||
const diff = objectDiff<Record<string, string[]>>(lastContent, content);
|
||||
// filter out keys which changed by reference only by checking whether the sets differ
|
||||
const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k]));
|
||||
// DM tag changes, refresh relevant rooms
|
||||
new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => {
|
||||
const room = this.matrixClient?.getRoom(roomId);
|
||||
if (room) {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
protected async reset() {
|
||||
this.rootSpaces = [];
|
||||
this.orphanedRooms = new Set();
|
||||
this.parentMap = new EnhancedMap();
|
||||
this.notificationStateMap = new Map();
|
||||
this.spaceFilteredRooms = new Map();
|
||||
@ -518,6 +601,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
this.matrixClient.removeListener("Room", this.onRoom);
|
||||
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
|
||||
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
|
||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
|
||||
this.matrixClient.removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
}
|
||||
await this.reset();
|
||||
}
|
||||
@ -527,6 +614,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
this.matrixClient.on("Room", this.onRoom);
|
||||
this.matrixClient.on("Room.myMembership", this.onRoom);
|
||||
this.matrixClient.on("RoomState.events", this.onRoomState);
|
||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
|
||||
this.matrixClient.on("accountData", this.onAccountData);
|
||||
}
|
||||
|
||||
await this.onSpaceUpdate(); // trigger an initial update
|
||||
|
||||
@ -551,7 +642,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
// Don't context switch when navigating to the space room
|
||||
// as it will cause you to end up in the wrong room
|
||||
this.setActiveSpace(room, false);
|
||||
} else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
|
||||
} else if (
|
||||
(!SettingsStore.getValue("feature_spaces.all_rooms") || this.activeSpace) &&
|
||||
!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)
|
||||
) {
|
||||
this.switchToRelatedSpace(roomId);
|
||||
}
|
||||
|
||||
@ -575,7 +669,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
}
|
||||
}
|
||||
|
||||
public getNotificationState(key: string): SpaceNotificationState {
|
||||
public getNotificationState(key: SpaceKey): SpaceNotificationState {
|
||||
if (this.notificationStateMap.has(key)) {
|
||||
return this.notificationStateMap.get(key);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomListStoreClass } from "./RoomListStore";
|
||||
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
/**
|
||||
* Watches for changes in spaces to manage the filter on the provided RoomListStore
|
||||
@ -28,6 +29,11 @@ export class SpaceWatcher {
|
||||
private activeSpace: Room = SpaceStore.instance.activeSpace;
|
||||
|
||||
constructor(private store: RoomListStoreClass) {
|
||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
this.filter = new SpaceFilterCondition();
|
||||
this.updateFilter();
|
||||
store.addFilter(this.filter);
|
||||
}
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
|
||||
}
|
||||
|
||||
@ -35,7 +41,7 @@ export class SpaceWatcher {
|
||||
this.activeSpace = activeSpace;
|
||||
|
||||
if (this.filter) {
|
||||
if (activeSpace) {
|
||||
if (activeSpace || !SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||
this.updateFilter();
|
||||
} else {
|
||||
this.store.removeFilter(this.filter);
|
||||
@ -49,9 +55,11 @@ export class SpaceWatcher {
|
||||
};
|
||||
|
||||
private updateFilter = () => {
|
||||
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
|
||||
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
|
||||
});
|
||||
if (this.activeSpace) {
|
||||
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
|
||||
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
|
||||
});
|
||||
}
|
||||
this.filter.updateSpace(this.activeSpace);
|
||||
};
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
|
||||
import { IDestroyable } from "../../../utils/IDestroyable";
|
||||
import SpaceStore from "../../SpaceStore";
|
||||
import SpaceStore, { HOME_SPACE } from "../../SpaceStore";
|
||||
import { setHasDiff } from "../../../utils/sets";
|
||||
|
||||
/**
|
||||
@ -29,7 +29,7 @@ import { setHasDiff } from "../../../utils/sets";
|
||||
* + All DMs
|
||||
*/
|
||||
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
|
||||
private roomIds = new Set<Room>();
|
||||
private roomIds = new Set<string>();
|
||||
private space: Room = null;
|
||||
|
||||
public get kind(): FilterKind {
|
||||
@ -55,12 +55,10 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
|
||||
}
|
||||
};
|
||||
|
||||
private getSpaceEventKey = (space: Room) => space.roomId;
|
||||
private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
|
||||
|
||||
public updateSpace(space: Room) {
|
||||
if (this.space) {
|
||||
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
||||
}
|
||||
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
||||
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
|
||||
this.onStoreUpdate(); // initial update from the change to the space
|
||||
}
|
||||
|
@ -1,30 +1,31 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import DMRoomMap from './DMRoomMap';
|
||||
|
||||
/* For now, a cut-down type spec for the client */
|
||||
interface Client {
|
||||
getUserId: () => string;
|
||||
checkUserTrust: (userId: string) => {
|
||||
isCrossSigningVerified: () => boolean
|
||||
wasCrossSigningVerified: () => boolean
|
||||
};
|
||||
getStoredDevicesForUser: (userId: string) => [{ deviceId: string }];
|
||||
checkDeviceTrust: (userId: string, deviceId: string) => {
|
||||
isVerified: () => boolean
|
||||
};
|
||||
}
|
||||
|
||||
interface Room {
|
||||
getEncryptionTargetMembers: () => Promise<[{userId: string}]>;
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export enum E2EStatus {
|
||||
Warning = "warning",
|
||||
Verified = "verified",
|
||||
Normal = "normal"
|
||||
}
|
||||
|
||||
export async function shieldStatusForRoom(client: Client, room: Room): Promise<E2EStatus> {
|
||||
export async function shieldStatusForRoom(client: MatrixClient, room: Room): Promise<E2EStatus> {
|
||||
const members = (await room.getEncryptionTargetMembers()).map(({userId}) => userId);
|
||||
const inDMMap = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
|
||||
|
@ -16,19 +16,20 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import * as url from "url";
|
||||
import { Capability, IWidget, IWidgetData, MatrixCapabilities } from "matrix-widget-api";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
import WidgetEchoStore from '../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {WidgetType} from "../widgets/WidgetType";
|
||||
import {objectClone} from "./objects";
|
||||
import {_t} from "../languageHandler";
|
||||
import {Capability, IWidget, IWidgetData, MatrixCapabilities} from "matrix-widget-api";
|
||||
import {IApp} from "../stores/WidgetStore";
|
||||
import { IntegrationManagers } from "../integrations/IntegrationManagers";
|
||||
import { WidgetType } from "../widgets/WidgetType";
|
||||
import { objectClone } from "./objects";
|
||||
import { _t } from "../languageHandler";
|
||||
import { IApp } from "../stores/WidgetStore";
|
||||
|
||||
// How long we wait for the state event echo to come back from the server
|
||||
// before waitFor[Room/User]Widget rejects its promise
|
||||
@ -377,9 +378,9 @@ export default class WidgetUtils {
|
||||
return widgets.filter(w => w.content && w.content.type === "m.integration_manager");
|
||||
}
|
||||
|
||||
static getRoomWidgetsOfType(room: Room, type: WidgetType): IWidgetEvent[] {
|
||||
const widgets = WidgetUtils.getRoomWidgets(room);
|
||||
return (widgets || []).filter(w => {
|
||||
static getRoomWidgetsOfType(room: Room, type: WidgetType): MatrixEvent[] {
|
||||
const widgets = WidgetUtils.getRoomWidgets(room) || [];
|
||||
return widgets.filter(w => {
|
||||
const content = w.getContent();
|
||||
return content.url && type.matches(content.type);
|
||||
});
|
||||
|
@ -42,7 +42,7 @@ import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
let client;
|
||||
const room = new Matrix.Room();
|
||||
const room = new Matrix.Room("!roomId:server_name");
|
||||
|
||||
// wrap MessagePanel with a component which provides the MatrixClient in the context.
|
||||
class WrappedMessagePanel extends React.Component {
|
||||
|
@ -123,8 +123,15 @@ describe("SpaceStore", () => {
|
||||
jest.runAllTimers();
|
||||
client.getVisibleRooms.mockReturnValue(rooms = []);
|
||||
getValue.mockImplementation(settingName => {
|
||||
if (settingName === "feature_spaces") {
|
||||
return true;
|
||||
switch (settingName) {
|
||||
case "feature_spaces":
|
||||
return true;
|
||||
case "feature_spaces.all_rooms":
|
||||
return true;
|
||||
case "feature_spaces.space_member_dms":
|
||||
return true;
|
||||
case "feature_spaces.space_dm_badges":
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user