Merge branch 'develop' into forgot-password-validation
This commit is contained in:
commit
901d5a86d0
189 changed files with 5819 additions and 2184 deletions
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
|
@ -39,6 +39,7 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
|||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||
import VoipUserMapper from "../VoipUserMapper";
|
||||
import {SpaceStoreClass} from "../stores/SpaceStore";
|
||||
import {VoiceRecorder} from "../voice/VoiceRecorder";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -70,6 +71,7 @@ declare global {
|
|||
mxModalWidgetStore: ModalWidgetStore;
|
||||
mxVoipUserMapper: VoipUserMapper;
|
||||
mxSpaceStore: SpaceStoreClass;
|
||||
mxVoiceRecorder: typeof VoiceRecorder;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
|
|
|
@ -14,27 +14,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
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 {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import {mediaFromMxc} from "./customisations/Media";
|
||||
|
||||
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, width: number, height: number, resizeMethod: ResizeMethod) {
|
||||
let url: string;
|
||||
if (member && member.getAvatarUrl) {
|
||||
url = member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
if (member?.getMxcAvatarUrl()) {
|
||||
url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
|
||||
Math.floor(width * window.devicePixelRatio),
|
||||
Math.floor(height * window.devicePixelRatio),
|
||||
resizeMethod,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (!url) {
|
||||
|
@ -47,16 +43,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu
|
|||
}
|
||||
|
||||
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||
const url = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||
if (!user.avatarUrl) return null;
|
||||
return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(
|
||||
Math.floor(width * window.devicePixelRatio),
|
||||
Math.floor(height * window.devicePixelRatio),
|
||||
resizeMethod,
|
||||
);
|
||||
if (!url || url.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function isValidHexColor(color: string): boolean {
|
||||
|
@ -154,15 +146,8 @@ export function getInitialLetter(name: string): string {
|
|||
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||
if (!room) return null; // null-guard
|
||||
|
||||
const explicitRoomAvatar = room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
false,
|
||||
);
|
||||
if (explicitRoomAvatar) {
|
||||
return explicitRoomAvatar;
|
||||
if (room.getMxcAvatarUrl()) {
|
||||
return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
}
|
||||
|
||||
// space rooms cannot be DMs so skip the rest
|
||||
|
@ -177,14 +162,8 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
|
|||
// then still try to show any avatar (pref. other member)
|
||||
otherMember = room.getAvatarFallbackMember();
|
||||
}
|
||||
if (otherMember) {
|
||||
return otherMember.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
false,
|
||||
);
|
||||
if (otherMember?.getMxcAvatarUrl()) {
|
||||
return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -788,6 +788,11 @@ export default class CallHandler {
|
|||
// don't remove the call yet: let the hangup event handler do it (otherwise it will throw
|
||||
// the hangup event away)
|
||||
break;
|
||||
case 'hangup_all':
|
||||
for (const call of this.calls.values()) {
|
||||
call.hangup(CallErrorCode.UserHangup, false);
|
||||
}
|
||||
break;
|
||||
case 'answer': {
|
||||
if (!this.calls.has(payload.room_id)) {
|
||||
return; // no call to answer
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {SettingLevel} from "./settings/SettingLevel";
|
||||
import {setMatrixCallAudioInput, setMatrixCallAudioOutput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export default {
|
||||
hasAnyLabeledDevices: async function() {
|
||||
|
@ -54,24 +54,24 @@ export default {
|
|||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||
|
||||
Matrix.setMatrixCallAudioOutput(audioOutDeviceId);
|
||||
Matrix.setMatrixCallAudioInput(audioDeviceId);
|
||||
Matrix.setMatrixCallVideoInput(videoDeviceId);
|
||||
setMatrixCallAudioOutput(audioOutDeviceId);
|
||||
setMatrixCallAudioInput(audioDeviceId);
|
||||
setMatrixCallVideoInput(videoDeviceId);
|
||||
},
|
||||
|
||||
setAudioOutput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||
Matrix.setMatrixCallAudioOutput(deviceId);
|
||||
setMatrixCallAudioOutput(deviceId);
|
||||
},
|
||||
|
||||
setAudioInput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||
Matrix.setMatrixCallAudioInput(deviceId);
|
||||
setMatrixCallAudioInput(deviceId);
|
||||
},
|
||||
|
||||
setVideoInput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||
Matrix.setMatrixCallVideoInput(deviceId);
|
||||
setMatrixCallVideoInput(deviceId);
|
||||
},
|
||||
|
||||
getAudioOutput: function() {
|
||||
|
|
|
@ -32,10 +32,10 @@ import { AllHtmlEntities } from 'html-entities';
|
|||
import SettingsStore from './settings/SettingsStore';
|
||||
import cheerio from 'cheerio';
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||
import {mediaFromMxc} from "./customisations/Media";
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -181,11 +181,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
|
|||
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
|
||||
return { tagName, attribs: {}};
|
||||
}
|
||||
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
attribs.src,
|
||||
attribs.width || 800,
|
||||
attribs.height || 600,
|
||||
);
|
||||
const width = Number(attribs.width) || 800;
|
||||
const height = Number(attribs.height) || 600;
|
||||
attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height);
|
||||
return { tagName, attribs };
|
||||
},
|
||||
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||
|
@ -239,6 +237,7 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
|
|||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub',
|
||||
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
|
||||
'details', 'summary',
|
||||
],
|
||||
allowedAttributes: {
|
||||
// custom ones first:
|
||||
|
|
|
@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { createClient, SERVICE_TYPES } from 'matrix-js-sdk';
|
||||
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import Modal from './Modal';
|
||||
|
|
|
@ -17,8 +17,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
|
||||
|
@ -219,7 +218,7 @@ export function attemptTokenLogin(
|
|||
button: _t("Try again"),
|
||||
onFinished: tryAgain => {
|
||||
if (tryAgain) {
|
||||
const cli = Matrix.createClient({
|
||||
const cli = createClient({
|
||||
baseUrl: homeserver,
|
||||
idBaseUrl: identityServer,
|
||||
});
|
||||
|
@ -276,7 +275,7 @@ function registerAsGuest(
|
|||
console.log(`Doing guest login on ${hsUrl}`);
|
||||
|
||||
// create a temporary MatrixClient to do the login
|
||||
const client = Matrix.createClient({
|
||||
const client = createClient({
|
||||
baseUrl: hsUrl,
|
||||
});
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||
import Matrix from "matrix-js-sdk";
|
||||
import {createClient} from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
|
@ -115,7 +115,7 @@ export default class Login {
|
|||
*/
|
||||
public createTemporaryClient(): MatrixClient {
|
||||
if (this.tempClient) return this.tempClient; // use memoization
|
||||
return this.tempClient = Matrix.createClient({
|
||||
return this.tempClient = createClient({
|
||||
baseUrl: this.hsUrl,
|
||||
idBaseUrl: this.isUrl,
|
||||
});
|
||||
|
@ -210,7 +210,7 @@ export async function sendLoginRequest(
|
|||
loginType: string,
|
||||
loginParams: ILoginParams,
|
||||
): Promise<IMatrixClientCreds> {
|
||||
const client = Matrix.createClient({
|
||||
const client = createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
|
|
|
@ -261,7 +261,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
|||
}
|
||||
|
||||
public getHomeserverName(): string {
|
||||
const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId);
|
||||
const matches = /^@[^:]+:(.+)$/.exec(this.matrixClient.credentials.userId);
|
||||
if (matches === null || matches.length < 1) {
|
||||
throw new Error("Failed to derive homeserver name from user ID!");
|
||||
}
|
||||
|
@ -296,10 +296,11 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
|||
// These are always installed regardless of the labs flag so that
|
||||
// cross-signing features can toggle on without reloading and also be
|
||||
// accessed immediately after login.
|
||||
const customisedCallbacks = {
|
||||
getDehydrationKey: SecurityCustomisations.getDehydrationKey,
|
||||
};
|
||||
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks, customisedCallbacks);
|
||||
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
|
||||
if (SecurityCustomisations.getDehydrationKey) {
|
||||
opts.cryptoCallbacks.getDehydrationKey =
|
||||
SecurityCustomisations.getDehydrationKey;
|
||||
}
|
||||
|
||||
this.matrixClient = createMatrixClient(opts);
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import {SettingLevel} from "./settings/SettingLevel";
|
|||
import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import UserActivity from "./UserActivity";
|
||||
import {mediaFromMxc} from "./customisations/Media";
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
|
@ -150,7 +151,7 @@ export const Notifier = {
|
|||
// Ideally in here we could use MSC1310 to detect the type of file, and reject it.
|
||||
|
||||
return {
|
||||
url: MatrixClientPeg.get().mxcUrlToHttp(content.url),
|
||||
url: mediaFromMxc(content.url).srcHttp,
|
||||
name: content.name,
|
||||
type: content.type,
|
||||
size: content.size,
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
/**
|
||||
|
@ -32,7 +32,7 @@ export default class PasswordReset {
|
|||
* @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
|
||||
*/
|
||||
constructor(homeserverUrl, identityUrl) {
|
||||
this.client = Matrix.createClient({
|
||||
this.client = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
idBaseUrl: identityUrl,
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import { EventStatus } from 'matrix-js-sdk';
|
||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
export default class Resend {
|
||||
static resendUnsentEvents(room) {
|
||||
|
|
|
@ -49,11 +49,12 @@ export function showStartChatInviteDialog(initialText) {
|
|||
);
|
||||
}
|
||||
|
||||
export function showRoomInviteDialog(roomId) {
|
||||
export function showRoomInviteDialog(roomId, initialText = "") {
|
||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||
Modal.createTrackedDialog(
|
||||
"Invite Users", "", InviteDialog, {
|
||||
kind: KIND_INVITE,
|
||||
initialText,
|
||||
roomId,
|
||||
},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
|
|
|
@ -21,9 +21,9 @@ import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
|
|||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import request from "browser-request";
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import {WidgetType} from "./widgets/WidgetType";
|
||||
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
|
||||
|
||||
// The version of the integration manager API we're intending to work with
|
||||
const imApiVersion = "1.1";
|
||||
|
@ -153,7 +153,7 @@ export default class ScalarAuthClient {
|
|||
parsedImRestUrl.path = '';
|
||||
parsedImRestUrl.pathname = '';
|
||||
return startTermsFlow([new Service(
|
||||
Matrix.SERVICE_TYPES.IM,
|
||||
SERVICE_TYPES.IM,
|
||||
parsedImRestUrl.format(),
|
||||
token,
|
||||
)], this.termsInteractionCallback).then(() => {
|
||||
|
|
|
@ -237,7 +237,7 @@ Example:
|
|||
*/
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import { MatrixEvent } from 'matrix-js-sdk';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import RoomViewStore from './stores/RoomViewStore';
|
||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ContentHelpers } from 'matrix-js-sdk';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
|
@ -126,10 +127,10 @@ export class Command {
|
|||
return this.getCommand() + " " + this.args;
|
||||
}
|
||||
|
||||
run(roomId: string, args: string, cmd: string) {
|
||||
run(roomId: string, args: string) {
|
||||
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
||||
if (!this.runFn) return reject(_t("Command error"));
|
||||
return this.runFn.bind(this)(roomId, args, cmd);
|
||||
return this.runFn.bind(this)(roomId, args);
|
||||
}
|
||||
|
||||
getUsage() {
|
||||
|
@ -163,7 +164,7 @@ export const Commands = [
|
|||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
|
||||
return success(ContentHelpers.makeTextMessage(message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -176,7 +177,7 @@ export const Commands = [
|
|||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
|
||||
return success(ContentHelpers.makeTextMessage(message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -189,7 +190,7 @@ export const Commands = [
|
|||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
|
||||
return success(ContentHelpers.makeTextMessage(message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -202,7 +203,7 @@ export const Commands = [
|
|||
if (args) {
|
||||
message = message + ' ' + args;
|
||||
}
|
||||
return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
|
||||
return success(ContentHelpers.makeTextMessage(message));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -211,7 +212,7 @@ export const Commands = [
|
|||
args: '<message>',
|
||||
description: _td('Sends a message as plain text, without interpreting it as markdown'),
|
||||
runFn: function(roomId, messages) {
|
||||
return success(MatrixClientPeg.get().sendTextMessage(roomId, messages));
|
||||
return success(ContentHelpers.makeTextMessage(messages));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -220,7 +221,7 @@ export const Commands = [
|
|||
args: '<message>',
|
||||
description: _td('Sends a message as html, without interpreting it as markdown'),
|
||||
runFn: function(roomId, messages) {
|
||||
return success(MatrixClientPeg.get().sendHtmlMessage(roomId, messages, messages));
|
||||
return success(ContentHelpers.makeHtmlMessage(messages, messages));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -965,7 +966,7 @@ export const Commands = [
|
|||
args: '<message>',
|
||||
runFn: function(roomId, args) {
|
||||
if (!args) return reject(this.getUserId());
|
||||
return success(MatrixClientPeg.get().sendHtmlMessage(roomId, args, textToHtmlRainbow(args)));
|
||||
return success(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -975,7 +976,7 @@ export const Commands = [
|
|||
args: '<message>',
|
||||
runFn: function(roomId, args) {
|
||||
if (!args) return reject(this.getUserId());
|
||||
return success(MatrixClientPeg.get().sendHtmlEmote(roomId, args, textToHtmlRainbow(args)));
|
||||
return success(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
|
||||
},
|
||||
category: CommandCategories.messages,
|
||||
}),
|
||||
|
@ -1200,10 +1201,13 @@ export function parseCommandString(input: string) {
|
|||
* processing the command, or 'promise' if a request was sent out.
|
||||
* Returns null if the input didn't match a command.
|
||||
*/
|
||||
export function getCommand(roomId: string, input: string) {
|
||||
export function getCommand(input: string) {
|
||||
const {cmd, args} = parseCommandString(input);
|
||||
|
||||
if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) {
|
||||
return () => CommandMap.get(cmd).run(roomId, args, cmd);
|
||||
return {
|
||||
cmd: CommandMap.get(cmd),
|
||||
args,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import React, {createRef} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
|
||||
import * as sdk from '../../../../index';
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
|
||||
import * as sdk from '../../../../index';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
|
|
|
@ -27,6 +27,7 @@ import {sortBy} from "lodash";
|
|||
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
|
||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||
import FlairStore from "../stores/FlairStore";
|
||||
import {mediaFromMxc} from "../customisations/Media";
|
||||
|
||||
const COMMUNITY_REGEX = /\B\+\S*/g;
|
||||
|
||||
|
@ -95,7 +96,7 @@ export default class CommunityProvider extends AutocompleteProvider {
|
|||
name={name || groupId}
|
||||
width={24}
|
||||
height={24}
|
||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
|
||||
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} />
|
||||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {Filter} from 'matrix-js-sdk';
|
||||
import {Filter} from 'matrix-js-sdk/src/filter';
|
||||
import * as sdk from '../../index';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import EventIndexPeg from "../../indexing/EventIndexPeg";
|
||||
|
|
|
@ -35,10 +35,11 @@ import GroupStore from '../../stores/GroupStore';
|
|||
import FlairStore from '../../stores/FlairStore';
|
||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
||||
import {Group} from "matrix-js-sdk";
|
||||
import {Group} from "matrix-js-sdk/src/models/group";
|
||||
import {allSettled, sleep} from "../../utils/promise";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import {mediaFromMxc} from "../../customisations/Media";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
|
@ -368,8 +369,7 @@ class FeaturedUser extends React.Component {
|
|||
|
||||
const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
|
||||
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
||||
const httpUrl = MatrixClientPeg.get()
|
||||
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
|
||||
const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64);
|
||||
|
||||
const deleteButton = this.props.editing ?
|
||||
<img
|
||||
|
@ -981,10 +981,9 @@ export default class GroupView extends React.Component {
|
|||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
const httpInviterAvatar = this.state.inviterProfile ?
|
||||
this._matrixClient.mxcUrlToHttp(
|
||||
this.state.inviterProfile.avatarUrl, 36, 36,
|
||||
) : null;
|
||||
const httpInviterAvatar = this.state.inviterProfile
|
||||
? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
|
||||
: null;
|
||||
|
||||
const inviter = group.inviter || {};
|
||||
let inviterName = inviter.userId;
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {InteractiveAuth} from "matrix-js-sdk";
|
||||
import {InteractiveAuth} from "matrix-js-sdk/src/interactive-auth";
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
|
|
@ -16,9 +16,11 @@ limitations under the License.
|
|||
|
||||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import GroupFilterPanel from "./GroupFilterPanel";
|
||||
import CustomRoomTagPanel from "./CustomRoomTagPanel";
|
||||
import classNames from "classnames";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
import RoomList from "../views/rooms/RoomList";
|
||||
|
@ -36,11 +38,11 @@ import {Key} from "../../Keyboard";
|
|||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||
import LeftPanelWidget from "./LeftPanelWidget";
|
||||
import SpacePanel from "../views/spaces/SpacePanel";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../customisations/Media";
|
||||
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -50,6 +52,7 @@ interface IProps {
|
|||
interface IState {
|
||||
showBreadcrumbs: boolean;
|
||||
showGroupFilterPanel: boolean;
|
||||
activeSpace?: Room;
|
||||
}
|
||||
|
||||
// List of CSS classes which should be included in keyboard navigation within the room list
|
||||
|
@ -75,11 +78,13 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
this.state = {
|
||||
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
||||
showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
|
||||
activeSpace: SpaceStore.instance.activeSpace,
|
||||
};
|
||||
|
||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||
this.bgImageWatcherRef = SettingsStore.watchSetting(
|
||||
"RoomList.backgroundImage", null, this.onBackgroundImageUpdate);
|
||||
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||
|
@ -97,9 +102,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
||||
}
|
||||
|
||||
private updateActiveSpace = (activeSpace: Room) => {
|
||||
this.setState({ activeSpace });
|
||||
};
|
||||
|
||||
private onExplore = () => {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
};
|
||||
|
@ -121,7 +131,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
||||
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
|
||||
if (settingBgMxc) {
|
||||
avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize);
|
||||
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
|
||||
}
|
||||
|
||||
const avatarUrlProp = `url(${avatarUrl})`;
|
||||
|
@ -382,7 +392,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
onEnter={this.onEnter}
|
||||
/>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_LeftPanel_exploreButton"
|
||||
className={classNames("mx_LeftPanel_exploreButton", {
|
||||
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
|
||||
})}
|
||||
onClick={this.onExplore}
|
||||
title={_t("Explore rooms")}
|
||||
/>
|
||||
|
@ -392,11 +404,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
|
||||
public render(): React.ReactNode {
|
||||
let leftLeftPanel;
|
||||
// Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now
|
||||
// ignore it and force the rendering of SpacePanel if that Labs flag is enabled.
|
||||
if (SettingsStore.getValue("feature_spaces")) {
|
||||
leftLeftPanel = <SpacePanel />;
|
||||
} else if (this.state.showGroupFilterPanel) {
|
||||
if (this.state.showGroupFilterPanel) {
|
||||
leftLeftPanel = (
|
||||
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
||||
<GroupFilterPanel />
|
||||
|
@ -412,6 +420,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
onBlur={this.onBlur}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.onResize}
|
||||
activeSpace={this.state.activeSpace}
|
||||
/>;
|
||||
|
||||
const containerClasses = classNames({
|
||||
|
|
|
@ -56,6 +56,7 @@ import Modal from "../../Modal";
|
|||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||
import { IOpts } from "../../createRoom";
|
||||
import SpacePanel from "../views/spaces/SpacePanel";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
|
@ -73,7 +74,6 @@ function canElementReceiveInput(el) {
|
|||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
||||
viaServers?: string[];
|
||||
hideToSRUsers: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
// eslint-disable-next-line camelcase
|
||||
|
@ -142,9 +142,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
// transitioned to PWLU)
|
||||
onRegistered: PropTypes.func,
|
||||
|
||||
// Used by the RoomView to handle joining rooms
|
||||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
// and lots and lots of other stuff.
|
||||
};
|
||||
|
||||
|
@ -229,21 +226,15 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
let size;
|
||||
let collapsed;
|
||||
const collapseConfig: ICollapseConfig = {
|
||||
// TODO: the space panel currently does not have a fixed width,
|
||||
// just the headers at each level have a max-width of 150px
|
||||
// Taking 222px for the space panel for now,
|
||||
// so this will look slightly off for now,
|
||||
// depending on the depth of your space tree.
|
||||
// To fix this, we'll need to turn toggleSize
|
||||
// into a callback so it can be measured when starting the resize operation
|
||||
toggleSize: 222 + 68,
|
||||
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
||||
toggleSize: 206 - 50,
|
||||
onCollapsed: (_collapsed) => {
|
||||
collapsed = _collapsed;
|
||||
if (_collapsed) {
|
||||
dis.dispatch({action: "hide_left_panel"}, true);
|
||||
dis.dispatch({action: "hide_left_panel"});
|
||||
window.localStorage.setItem("mx_lhs_size", '0');
|
||||
} else {
|
||||
dis.dispatch({action: "show_left_panel"}, true);
|
||||
dis.dispatch({action: "show_left_panel"});
|
||||
}
|
||||
},
|
||||
onResized: (_size) => {
|
||||
|
@ -630,11 +621,9 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
case PageTypes.RoomView:
|
||||
pageElement = <RoomView
|
||||
ref={this._roomView}
|
||||
autoJoin={this.props.autoJoin}
|
||||
onRegistered={this.props.onRegistered}
|
||||
threepidInvite={this.props.threepidInvite}
|
||||
oobData={this.props.roomOobData}
|
||||
viaServers={this.props.viaServers}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
justCreatedOpts={this.props.roomJustCreatedOpts}
|
||||
|
@ -670,13 +659,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||
}
|
||||
|
||||
const leftPanel = (
|
||||
<LeftPanel
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<div
|
||||
|
@ -688,7 +670,11 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||
{ leftPanel }
|
||||
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
|
||||
<LeftPanel
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
<ResizeHandle />
|
||||
{ pageElement }
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017-2019 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015-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.
|
||||
|
@ -18,8 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
// @ts-ignore - XXX: no idea why this import fails
|
||||
import * as Matrix from "matrix-js-sdk";
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
@ -82,9 +78,12 @@ import {UIFeature} from "../../settings/UIFeature";
|
|||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
import DialPadModal from "../views/voip/DialPadModal";
|
||||
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
|
||||
import { shouldUseLoginForWelcome } from "../../utils/pages";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import SpaceRoomDirectory from "./SpaceRoomDirectory";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import {RoomUpdateCause} from "../../stores/room-list/models";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -203,7 +202,6 @@ interface IState {
|
|||
ready: boolean;
|
||||
threepidInvite?: IThreepidInvite,
|
||||
roomOobData?: object;
|
||||
viaServers?: string[];
|
||||
pendingInitialSync?: boolean;
|
||||
justRegistered?: boolean;
|
||||
roomJustCreatedOpts?: IOpts;
|
||||
|
@ -582,6 +580,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
break;
|
||||
case 'logout':
|
||||
dis.dispatch({action: "hangup_all"});
|
||||
Lifecycle.logout();
|
||||
break;
|
||||
case 'require_registration':
|
||||
|
@ -606,12 +605,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
if (payload.screenAfterLogin) {
|
||||
this.screenAfterLogin = payload.screenAfterLogin;
|
||||
}
|
||||
this.setStateForNewView({
|
||||
view: Views.LOGIN,
|
||||
});
|
||||
this.notifyNewScreen('login');
|
||||
ThemeController.isLogin = true;
|
||||
this.themeWatcher.recheck();
|
||||
this.viewLogin();
|
||||
break;
|
||||
case 'start_password_recovery':
|
||||
this.setStateForNewView({
|
||||
|
@ -934,7 +928,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
page_type: PageTypes.RoomView,
|
||||
threepidInvite: roomInfo.threepid_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
viaServers: roomInfo.via_servers,
|
||||
ready: true,
|
||||
roomJustCreatedOpts: roomInfo.justCreatedOpts,
|
||||
}, () => {
|
||||
|
@ -975,6 +968,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
private viewWelcome() {
|
||||
if (shouldUseLoginForWelcome(SdkConfig.get())) {
|
||||
return this.viewLogin();
|
||||
}
|
||||
this.setStateForNewView({
|
||||
view: Views.WELCOME,
|
||||
});
|
||||
|
@ -983,6 +979,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.themeWatcher.recheck();
|
||||
}
|
||||
|
||||
private viewLogin(otherState?: any) {
|
||||
this.setStateForNewView({
|
||||
view: Views.LOGIN,
|
||||
...otherState,
|
||||
});
|
||||
this.notifyNewScreen('login');
|
||||
ThemeController.isLogin = true;
|
||||
this.themeWatcher.recheck();
|
||||
}
|
||||
|
||||
private viewHome(justRegistered = false) {
|
||||
// The home page requires the "logged in" view, so we'll set that.
|
||||
this.setStateForNewView({
|
||||
|
@ -1139,11 +1145,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
private forgetRoom(roomId: string) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
MatrixClientPeg.get().forget(roomId).then(() => {
|
||||
// Switch to home page if we're currently viewing the forgotten room
|
||||
if (this.state.currentRoomId === roomId) {
|
||||
dis.dispatch({ action: "view_home_page" });
|
||||
}
|
||||
|
||||
// We have to manually update the room list because the forgotten room will not
|
||||
// be notified to us, therefore the room list will have no other way of knowing
|
||||
// the room is forgotten.
|
||||
RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||
}).catch((err) => {
|
||||
const errCode = err.errcode || _td("unknown error code");
|
||||
Modal.createTrackedDialog("Failed to forget room", '', ErrorDialog, {
|
||||
|
@ -1298,17 +1310,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
* Called when the session is logged out
|
||||
*/
|
||||
private onLoggedOut() {
|
||||
this.notifyNewScreen('login');
|
||||
this.setStateForNewView({
|
||||
view: Views.LOGIN,
|
||||
this.viewLogin({
|
||||
ready: false,
|
||||
collapseLhs: false,
|
||||
currentRoomId: null,
|
||||
});
|
||||
this.subTitleStatus = '';
|
||||
this.setPageSubtitle();
|
||||
ThemeController.isLogin = true;
|
||||
this.themeWatcher.recheck();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1648,7 +1656,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
let cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
const {hsUrl, isUrl} = this.props.serverConfig;
|
||||
cli = Matrix.createClient({
|
||||
cli = createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
|
|
|
@ -23,7 +23,6 @@ import classNames from 'classnames';
|
|||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import * as sdk from '../../index';
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
|
@ -47,6 +46,9 @@ function shouldFormContinuation(prevEvent, mxEvent) {
|
|||
// check if within the max continuation period
|
||||
if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false;
|
||||
|
||||
// As we summarise redactions, do not continue a redacted event onto a non-redacted one and vice-versa
|
||||
if (mxEvent.isRedacted() !== prevEvent.isRedacted()) return false;
|
||||
|
||||
// Some events should appear as continuations from previous events of different types.
|
||||
if (mxEvent.getType() !== prevEvent.getType() &&
|
||||
(!continuedTypes.includes(mxEvent.getType()) ||
|
||||
|
@ -210,13 +212,11 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -229,14 +229,6 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
switch (payload.action) {
|
||||
case "scroll_to_bottom":
|
||||
this.scrollToBottom();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onShowTypingNotificationsChange = () => {
|
||||
this.setState({
|
||||
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||
|
@ -463,6 +455,20 @@ export default class MessagePanel extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_getNextEventInfo(arr, i) {
|
||||
const nextEvent = i < arr.length - 1
|
||||
? arr[i + 1]
|
||||
: null;
|
||||
|
||||
// The next event with tile is used to to determine the 'last successful' flag
|
||||
// when rendering the tile. The shouldShowEvent function is pretty quick at what
|
||||
// it does, so this should have no significant cost even when a room is used for
|
||||
// not-chat purposes.
|
||||
const nextTile = arr.slice(i + 1).find(e => this._shouldShowEvent(e));
|
||||
|
||||
return {nextEvent, nextTile};
|
||||
}
|
||||
|
||||
_getEventTiles() {
|
||||
this.eventNodes = {};
|
||||
|
||||
|
@ -514,6 +520,7 @@ export default class MessagePanel extends React.Component {
|
|||
const mxEv = this.props.events[i];
|
||||
const eventId = mxEv.getId();
|
||||
const last = (mxEv === lastShownEvent);
|
||||
const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i);
|
||||
|
||||
if (grouper) {
|
||||
if (grouper.shouldGroup(mxEv)) {
|
||||
|
@ -530,22 +537,12 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
for (const Grouper of groupers) {
|
||||
if (Grouper.canStartGroup(this, mxEv)) {
|
||||
grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent);
|
||||
grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile);
|
||||
}
|
||||
}
|
||||
if (!grouper) {
|
||||
const wantTile = this._shouldShowEvent(mxEv);
|
||||
if (wantTile) {
|
||||
const nextEvent = i < this.props.events.length - 1
|
||||
? this.props.events[i + 1]
|
||||
: null;
|
||||
|
||||
// The next event with tile is used to to determine the 'last successful' flag
|
||||
// when rendering the tile. The shouldShowEvent function is pretty quick at what
|
||||
// it does, so this should have no significant cost even when a room is used for
|
||||
// not-chat purposes.
|
||||
const nextTile = this.props.events.slice(i + 1).find(e => this._shouldShowEvent(e));
|
||||
|
||||
// make sure we unpack the array returned by _getTilesForEvent,
|
||||
// otherwise react will auto-generate keys and we will end up
|
||||
// replacing all of the DOM elements every time we paginate.
|
||||
|
@ -1038,6 +1035,103 @@ class CreationGrouper {
|
|||
}
|
||||
}
|
||||
|
||||
class RedactionGrouper {
|
||||
static canStartGroup = function(panel, ev) {
|
||||
return panel._shouldShowEvent(ev) && ev.isRedacted();
|
||||
}
|
||||
|
||||
constructor(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile) {
|
||||
this.panel = panel;
|
||||
this.readMarker = panel._readMarkerForEvent(
|
||||
ev.getId(),
|
||||
ev === lastShownEvent,
|
||||
);
|
||||
this.events = [ev];
|
||||
this.prevEvent = prevEvent;
|
||||
this.lastShownEvent = lastShownEvent;
|
||||
this.nextEvent = nextEvent;
|
||||
this.nextEventTile = nextEventTile;
|
||||
}
|
||||
|
||||
shouldGroup(ev) {
|
||||
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
|
||||
if (!this.panel._shouldShowEvent(ev)) {
|
||||
return true;
|
||||
}
|
||||
if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||
return false;
|
||||
}
|
||||
return ev.isRedacted();
|
||||
}
|
||||
|
||||
add(ev) {
|
||||
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
|
||||
ev.getId(),
|
||||
ev === this.lastShownEvent,
|
||||
);
|
||||
if (!this.panel._shouldShowEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
this.events.push(ev);
|
||||
}
|
||||
|
||||
getTiles() {
|
||||
if (!this.events || !this.events.length) return [];
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
|
||||
const panel = this.panel;
|
||||
const ret = [];
|
||||
const lastShownEvent = this.lastShownEvent;
|
||||
|
||||
if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
|
||||
const ts = this.events[0].getTs();
|
||||
ret.push(
|
||||
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
|
||||
);
|
||||
}
|
||||
|
||||
const key = "redactioneventlistsummary-" + (
|
||||
this.prevEvent ? this.events[0].getId() : "initial"
|
||||
);
|
||||
|
||||
const senders = new Set();
|
||||
let eventTiles = this.events.map((e, i) => {
|
||||
senders.add(e.sender);
|
||||
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
|
||||
return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile);
|
||||
}).reduce((a, b) => a.concat(b), []);
|
||||
|
||||
if (eventTiles.length === 0) {
|
||||
eventTiles = null;
|
||||
}
|
||||
|
||||
ret.push(
|
||||
<EventListSummary
|
||||
key={key}
|
||||
threshold={2}
|
||||
events={this.events}
|
||||
onToggle={panel._onHeightChanged} // Update scroll state
|
||||
summaryMembers={Array.from(senders)}
|
||||
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
|
||||
>
|
||||
{ eventTiles }
|
||||
</EventListSummary>,
|
||||
);
|
||||
|
||||
if (this.readMarker) {
|
||||
ret.push(this.readMarker);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
getNewPrevEvent() {
|
||||
return this.events[this.events.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
class MemberGrouper {
|
||||
static canStartGroup = function(panel, ev) {
|
||||
|
@ -1148,4 +1242,4 @@ class MemberGrouper {
|
|||
}
|
||||
|
||||
// all the grouper classes that we use
|
||||
const groupers = [CreationGrouper, MemberGrouper];
|
||||
const groupers = [CreationGrouper, MemberGrouper, RedactionGrouper];
|
||||
|
|
|
@ -27,7 +27,6 @@ import { _t } from '../../languageHandler';
|
|||
import SdkConfig from '../../SdkConfig';
|
||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||
import Analytics from '../../Analytics';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||
|
@ -35,6 +34,7 @@ import GroupStore from "../../stores/GroupStore";
|
|||
import FlairStore from "../../stores/FlairStore";
|
||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../customisations/Media";
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 800;
|
||||
|
@ -521,10 +521,9 @@ export default class RoomDirectory extends React.Component {
|
|||
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
|
||||
}
|
||||
topic = linkifyAndSanitizeHtml(topic);
|
||||
const avatarUrl = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
room.avatar_url, 32, 32, "crop",
|
||||
);
|
||||
let avatarUrl = null;
|
||||
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
|
||||
|
||||
return [
|
||||
<div key={ `${room.room_id}_avatar` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import Resend from '../../Resend';
|
||||
|
@ -24,6 +23,7 @@ import dis from '../../dispatcher/dispatcher';
|
|||
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import {EventStatus} from "matrix-js-sdk/src/models/event";
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -32,7 +32,7 @@ const STATUS_BAR_EXPANDED_LARGE = 2;
|
|||
function getUnsentMessages(room) {
|
||||
if (!room) { return []; }
|
||||
return room.getPendingEvents().filter(function(ev) {
|
||||
return ev.status === Matrix.EventStatus.NOT_SENT;
|
||||
return ev.status === EventStatus.NOT_SENT;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -112,10 +112,6 @@ interface IProps {
|
|||
inviterName?: string;
|
||||
};
|
||||
|
||||
// Servers the RoomView can use to try and assist joins
|
||||
viaServers?: string[];
|
||||
|
||||
autoJoin?: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
justCreatedOpts?: IOpts;
|
||||
|
||||
|
@ -450,9 +446,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
// now not joined because the js-sdk peeking API will clobber our historical room,
|
||||
// making it impossible to indicate a newly joined room.
|
||||
if (!joining && roomId) {
|
||||
if (this.props.autoJoin) {
|
||||
this.onJoinButtonClicked();
|
||||
} else if (!room && shouldPeek) {
|
||||
if (!room && shouldPeek) {
|
||||
console.info("Attempting to peek into room %s", roomId);
|
||||
this.setState({
|
||||
peekLoading: true,
|
||||
|
@ -1123,7 +1117,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
const signUrl = this.props.threepidInvite?.signUrl;
|
||||
dis.dispatch({
|
||||
action: 'join_room',
|
||||
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
_type: "unknown", // TODO: instrumentation
|
||||
});
|
||||
return Promise.resolve();
|
||||
|
|
|
@ -32,6 +32,8 @@ export default class SearchBox extends React.Component {
|
|||
onKeyDown: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
autoFocus: PropTypes.bool,
|
||||
initialValue: PropTypes.string,
|
||||
|
||||
// If true, the search box will focus and clear itself
|
||||
// on room search focus action (it would be nicer to take
|
||||
|
@ -49,7 +51,7 @@ export default class SearchBox extends React.Component {
|
|||
this._search = createRef();
|
||||
|
||||
this.state = {
|
||||
searchTerm: "",
|
||||
searchTerm: this.props.initialValue || "",
|
||||
blurred: true,
|
||||
};
|
||||
}
|
||||
|
@ -158,6 +160,7 @@ export default class SearchBox extends React.Component {
|
|||
onBlur={this._onBlur}
|
||||
placeholder={ placeholder }
|
||||
autoComplete="off"
|
||||
autoFocus={this.props.autoFocus}
|
||||
/>
|
||||
{ clearButton }
|
||||
</div>
|
||||
|
|
|
@ -14,26 +14,31 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useMemo, useRef, useState} from "react";
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
||||
import React, {useMemo, useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
import classNames from "classnames";
|
||||
import {sortBy} from "lodash";
|
||||
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {_t} from "../../languageHandler";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
import FormButton from "../views/elements/FormButton";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import SearchBox from "./SearchBox";
|
||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
||||
import {shouldShowSpaceSettings} from "../../utils/space";
|
||||
import {EnhancedMap} from "../../utils/maps";
|
||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||
import {mediaFromMxc} from "../../customisations/Media";
|
||||
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -71,237 +76,131 @@ export interface ISpaceSummaryEvent {
|
|||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface ISubspaceProps {
|
||||
space: ISpaceSummaryRoom;
|
||||
event?: MatrixEvent;
|
||||
editing?: boolean;
|
||||
onPreviewClick?(): void;
|
||||
queueAction?(action: IAction): void;
|
||||
onJoinClick?(): void;
|
||||
interface ITileProps {
|
||||
room: ISpaceSummaryRoom;
|
||||
suggested?: boolean;
|
||||
selected?: boolean;
|
||||
numChildRooms?: number;
|
||||
hasPermissions?: boolean;
|
||||
onViewRoomClick(autoJoin: boolean): void;
|
||||
onToggleClick?(): void;
|
||||
}
|
||||
|
||||
const SubSpace: React.FC<ISubspaceProps> = ({
|
||||
space,
|
||||
editing,
|
||||
event,
|
||||
queueAction,
|
||||
onJoinClick,
|
||||
onPreviewClick,
|
||||
const Tile: React.FC<ITileProps> = ({
|
||||
room,
|
||||
suggested,
|
||||
selected,
|
||||
hasPermissions,
|
||||
onToggleClick,
|
||||
onViewRoomClick,
|
||||
numChildRooms,
|
||||
children,
|
||||
}) => {
|
||||
const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
|
||||
const name = room.name || room.canonical_alias || room.aliases?.[0]
|
||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||
|
||||
const evContent = event?.getContent();
|
||||
const [suggested, _setSuggested] = useState(evContent?.suggested);
|
||||
const [removed, _setRemoved] = useState(!evContent?.via);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cliRoom = cli.getRoom(space.room_id);
|
||||
const myMembership = cliRoom?.getMyMembership();
|
||||
|
||||
// TODO DRY code
|
||||
let actions;
|
||||
if (editing && queueAction) {
|
||||
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
|
||||
const setSuggested = () => {
|
||||
_setSuggested(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed,
|
||||
suggested: !v,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
};
|
||||
|
||||
const setRemoved = () => {
|
||||
_setRemoved(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed: !v,
|
||||
suggested,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
};
|
||||
|
||||
if (removed) {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
|
||||
<StyledCheckbox checked={suggested} onChange={setSuggested} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
} else {
|
||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
||||
{ _t("No permissions")}
|
||||
</span>;
|
||||
}
|
||||
// TODO confirm remove from space click behaviour here
|
||||
} else {
|
||||
if (myMembership === "join") {
|
||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
||||
{ _t("You're in this space")}
|
||||
</span>;
|
||||
} else if (onJoinClick) {
|
||||
actions = <React.Fragment>
|
||||
<AccessibleButton onClick={onPreviewClick} kind="link">
|
||||
{ _t("Preview") }
|
||||
</AccessibleButton>
|
||||
<FormButton onClick={onJoinClick} label={_t("Join")} />
|
||||
</React.Fragment>
|
||||
}
|
||||
}
|
||||
|
||||
let url: string;
|
||||
if (space.avatar_url) {
|
||||
url = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
space.avatar_url,
|
||||
Math.floor(24 * window.devicePixelRatio),
|
||||
Math.floor(24 * window.devicePixelRatio),
|
||||
"crop",
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomDirectory_subspace">
|
||||
<div className="mx_SpaceRoomDirectory_subspace_info">
|
||||
<BaseAvatar name={name} idName={space.room_id} url={url} width={24} height={24} />
|
||||
{ name }
|
||||
|
||||
<div className="mx_SpaceRoomDirectory_actions">
|
||||
{ actions }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_SpaceRoomDirectory_subspace_children">
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
interface IAction {
|
||||
event: MatrixEvent;
|
||||
suggested: boolean;
|
||||
removed: boolean;
|
||||
}
|
||||
|
||||
interface IRoomTileProps {
|
||||
room: ISpaceSummaryRoom;
|
||||
event?: MatrixEvent;
|
||||
editing?: boolean;
|
||||
onPreviewClick(): void;
|
||||
queueAction?(action: IAction): void;
|
||||
onJoinClick?(): void;
|
||||
}
|
||||
|
||||
const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinClick }: IRoomTileProps) => {
|
||||
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
|
||||
|
||||
const evContent = event?.getContent();
|
||||
const [suggested, _setSuggested] = useState(evContent?.suggested);
|
||||
const [removed, _setRemoved] = useState(!evContent?.via);
|
||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cliRoom = cli.getRoom(room.room_id);
|
||||
const myMembership = cliRoom?.getMyMembership();
|
||||
|
||||
let actions;
|
||||
if (editing && queueAction) {
|
||||
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
|
||||
const setSuggested = () => {
|
||||
_setSuggested(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed,
|
||||
suggested: !v,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
};
|
||||
const onPreviewClick = () => onViewRoomClick(false);
|
||||
const onJoinClick = () => onViewRoomClick(true);
|
||||
|
||||
const setRemoved = () => {
|
||||
_setRemoved(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed: !v,
|
||||
suggested,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
};
|
||||
let button;
|
||||
if (myMembership === "join") {
|
||||
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
||||
{ _t("Open") }
|
||||
</AccessibleButton>;
|
||||
} else if (onJoinClick) {
|
||||
button = <AccessibleButton onClick={onJoinClick} kind="primary">
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
|
||||
<StyledCheckbox checked={suggested} onChange={setSuggested} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
let checkbox;
|
||||
if (onToggleClick) {
|
||||
if (hasPermissions) {
|
||||
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} />;
|
||||
} else {
|
||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
||||
{ _t("No permissions")}
|
||||
</span>;
|
||||
}
|
||||
// TODO confirm remove from space click behaviour here
|
||||
} else {
|
||||
if (myMembership === "join") {
|
||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
||||
{ _t("You're in this room")}
|
||||
</span>;
|
||||
} else if (onJoinClick) {
|
||||
actions = <React.Fragment>
|
||||
<AccessibleButton onClick={onPreviewClick} kind="link">
|
||||
{ _t("Preview") }
|
||||
</AccessibleButton>
|
||||
<FormButton onClick={onJoinClick} label={_t("Join")} />
|
||||
</React.Fragment>
|
||||
checkbox = <TextWithTooltip
|
||||
tooltip={_t("You don't have permission")}
|
||||
onClick={ev => { ev.stopPropagation() }}
|
||||
>
|
||||
<StyledCheckbox disabled={true} />
|
||||
</TextWithTooltip>;
|
||||
}
|
||||
}
|
||||
|
||||
let url: string;
|
||||
if (room.avatar_url) {
|
||||
url = cli.mxcUrlToHttp(
|
||||
room.avatar_url,
|
||||
Math.floor(32 * window.devicePixelRatio),
|
||||
Math.floor(32 * window.devicePixelRatio),
|
||||
"crop",
|
||||
);
|
||||
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(20 * window.devicePixelRatio));
|
||||
}
|
||||
|
||||
let description = _t("%(count)s members", { count: room.num_joined_members });
|
||||
if (numChildRooms) {
|
||||
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
|
||||
}
|
||||
if (room.topic) {
|
||||
description += " · " + room.topic;
|
||||
}
|
||||
|
||||
let suggestedSection;
|
||||
if (suggested) {
|
||||
suggestedSection = <InfoTooltip tooltip={_t("This room is suggested as a good one to join")}>
|
||||
{ _t("Suggested") }
|
||||
</InfoTooltip>;
|
||||
}
|
||||
|
||||
const content = <React.Fragment>
|
||||
<BaseAvatar name={name} idName={room.room_id} url={url} width={32} height={32} />
|
||||
<BaseAvatar name={name} idName={room.room_id} url={url} width={20} height={20} />
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
||||
{ name }
|
||||
{ suggestedSection }
|
||||
</div>
|
||||
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_info">
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
||||
{ name }
|
||||
</div>
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_topic">
|
||||
{ room.topic }
|
||||
</div>
|
||||
{ description }
|
||||
</div>
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_memberCount">
|
||||
{ room.num_joined_members }
|
||||
</div>
|
||||
|
||||
<div className="mx_SpaceRoomDirectory_actions">
|
||||
{ actions }
|
||||
{ button }
|
||||
{ checkbox }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
if (editing) {
|
||||
return <div className="mx_SpaceRoomDirectory_roomTile">
|
||||
{ content }
|
||||
</div>
|
||||
let childToggle;
|
||||
let childSection;
|
||||
if (children) {
|
||||
// the chevron is purposefully a div rather than a button as it should be ignored for a11y
|
||||
childToggle = <div
|
||||
className={classNames("mx_SpaceRoomDirectory_subspace_toggle", {
|
||||
mx_SpaceRoomDirectory_subspace_toggle_shown: showChildren,
|
||||
})}
|
||||
onClick={ev => {
|
||||
ev.stopPropagation();
|
||||
toggleShowChildren();
|
||||
}}
|
||||
/>;
|
||||
if (showChildren) {
|
||||
childSection = <div className="mx_SpaceRoomDirectory_subspace_children">
|
||||
{ children }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return <AccessibleButton className="mx_SpaceRoomDirectory_roomTile" onClick={onPreviewClick}>
|
||||
{ content }
|
||||
</AccessibleButton>;
|
||||
return <>
|
||||
<AccessibleButton
|
||||
className={classNames("mx_SpaceRoomDirectory_roomTile", {
|
||||
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
|
||||
})}
|
||||
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
||||
>
|
||||
{ content }
|
||||
{ childToggle }
|
||||
</AccessibleButton>
|
||||
{ childSection }
|
||||
</>;
|
||||
};
|
||||
|
||||
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
|
||||
|
@ -334,98 +233,121 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi
|
|||
interface IHierarchyLevelProps {
|
||||
spaceId: string;
|
||||
rooms: Map<string, ISpaceSummaryRoom>;
|
||||
editing?: boolean;
|
||||
relations: EnhancedMap<string, string[]>;
|
||||
relations: Map<string, Map<string, ISpaceSummaryEvent>>;
|
||||
parents: Set<string>;
|
||||
queueAction?(action: IAction): void;
|
||||
onPreviewClick(roomId: string): void;
|
||||
onRemoveFromSpaceClick?(roomId: string): void;
|
||||
onJoinClick?(roomId: string): void;
|
||||
selectedMap?: Map<string, Set<string>>;
|
||||
onViewRoomClick(roomId: string, autoJoin: boolean): void;
|
||||
onToggleClick?(parentId: string, childId: string): void;
|
||||
}
|
||||
|
||||
export const HierarchyLevel = ({
|
||||
spaceId,
|
||||
rooms,
|
||||
editing,
|
||||
relations,
|
||||
parents,
|
||||
onPreviewClick,
|
||||
onJoinClick,
|
||||
queueAction,
|
||||
selectedMap,
|
||||
onViewRoomClick,
|
||||
onToggleClick,
|
||||
}: IHierarchyLevelProps) => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const space = cli.getRoom(spaceId);
|
||||
// TODO respect order
|
||||
const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => {
|
||||
if (!rooms.has(roomId)) return result; // TODO wat
|
||||
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())
|
||||
|
||||
const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null);
|
||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
||||
const roomId = ev.state_key;
|
||||
if (!rooms.has(roomId)) return result;
|
||||
result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId);
|
||||
return result;
|
||||
}, [[], []]) || [[], []];
|
||||
|
||||
// Don't render this subspace if it has no rooms we can show
|
||||
// TODO this is broken - as a space may have subspaces we still need to show
|
||||
// if (!childRooms.length) return null;
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const newParents = new Set(parents).add(spaceId);
|
||||
return <React.Fragment>
|
||||
{
|
||||
childRooms.map(roomId => (
|
||||
<RoomTile
|
||||
<Tile
|
||||
key={roomId}
|
||||
room={rooms.get(roomId)}
|
||||
event={space?.currentState.maySendStateEvent(EventType.SpaceChild, userId)
|
||||
? space?.currentState.getStateEvents(EventType.SpaceChild, roomId)
|
||||
: undefined}
|
||||
editing={editing}
|
||||
queueAction={queueAction}
|
||||
onPreviewClick={() => {
|
||||
onPreviewClick(roomId);
|
||||
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
|
||||
selected={selectedMap?.get(spaceId)?.has(roomId)}
|
||||
onViewRoomClick={(autoJoin) => {
|
||||
onViewRoomClick(roomId, autoJoin);
|
||||
}}
|
||||
onJoinClick={onJoinClick ? () => {
|
||||
onJoinClick(roomId);
|
||||
} : undefined}
|
||||
hasPermissions={hasPermissions}
|
||||
onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
{
|
||||
subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => (
|
||||
<SubSpace
|
||||
<Tile
|
||||
key={roomId}
|
||||
space={rooms.get(roomId)}
|
||||
event={space?.currentState.getStateEvents(EventType.SpaceChild, roomId)}
|
||||
editing={editing}
|
||||
queueAction={queueAction}
|
||||
onPreviewClick={() => {
|
||||
onPreviewClick(roomId);
|
||||
}}
|
||||
onJoinClick={() => {
|
||||
onJoinClick(roomId);
|
||||
room={rooms.get(roomId)}
|
||||
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
|
||||
.filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
|
||||
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
|
||||
selected={selectedMap?.get(spaceId)?.has(roomId)}
|
||||
onViewRoomClick={(autoJoin) => {
|
||||
onViewRoomClick(roomId, autoJoin);
|
||||
}}
|
||||
hasPermissions={hasPermissions}
|
||||
onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
|
||||
>
|
||||
<HierarchyLevel
|
||||
spaceId={roomId}
|
||||
rooms={rooms}
|
||||
editing={editing}
|
||||
relations={relations}
|
||||
parents={newParents}
|
||||
onPreviewClick={onPreviewClick}
|
||||
onJoinClick={onJoinClick}
|
||||
queueAction={queueAction}
|
||||
selectedMap={selectedMap}
|
||||
onViewRoomClick={onViewRoomClick}
|
||||
onToggleClick={onToggleClick}
|
||||
/>
|
||||
</SubSpace>
|
||||
</Tile>
|
||||
))
|
||||
}
|
||||
</React.Fragment>
|
||||
};
|
||||
|
||||
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
|
||||
// mutate argument refreshToken to force a reload
|
||||
export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
|
||||
ISpaceSummaryRoom[],
|
||||
Map<string, Map<string, ISpaceSummaryEvent>>,
|
||||
Map<string, Set<string>>,
|
||||
Map<string, Set<string>>,
|
||||
] | [] => {
|
||||
// TODO pagination
|
||||
return useAsyncMemo(async () => {
|
||||
try {
|
||||
const data = await cli.getSpaceSummary(space.roomId);
|
||||
|
||||
const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
|
||||
const childParentRelations = new EnhancedMap<string, Set<string>>();
|
||||
const viaMap = new EnhancedMap<string, Set<string>>();
|
||||
data.events.map((ev: ISpaceSummaryEvent) => {
|
||||
if (ev.type === EventType.SpaceChild) {
|
||||
parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
|
||||
childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id);
|
||||
}
|
||||
if (Array.isArray(ev.content["via"])) {
|
||||
const set = viaMap.getOrCreate(ev.state_key, new Set());
|
||||
ev.content["via"].forEach(via => set.add(via));
|
||||
}
|
||||
});
|
||||
|
||||
return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations];
|
||||
} catch (e) {
|
||||
console.error(e); // TODO
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [space, refreshToken], []);
|
||||
};
|
||||
|
||||
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getUserId();
|
||||
const [query, setQuery] = useState(initialText);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const onCreateRoomClick = () => {
|
||||
dis.dispatch({
|
||||
|
@ -435,116 +357,210 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
|
|||
onFinished();
|
||||
};
|
||||
|
||||
// stored within a ref as we don't need to re-render when it changes
|
||||
const pendingActions = useRef(new Map<string, IAction>());
|
||||
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
|
||||
|
||||
let adminButton;
|
||||
if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test
|
||||
const onManageButtonClicked = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const onSaveButtonClicked = () => {
|
||||
// TODO setBusy
|
||||
pendingActions.current.forEach(({event, suggested, removed}) => {
|
||||
const content = {
|
||||
...event.getContent(),
|
||||
suggested,
|
||||
};
|
||||
|
||||
if (removed) {
|
||||
delete content["via"];
|
||||
}
|
||||
|
||||
cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey());
|
||||
});
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
adminButton = <React.Fragment>
|
||||
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
|
||||
<span>{ _t("Promoted to users") }</span>
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;
|
||||
}
|
||||
}
|
||||
|
||||
const [rooms, relations, viaMap] = useAsyncMemo(async () => {
|
||||
try {
|
||||
const data = await cli.getSpaceSummary(space.roomId);
|
||||
|
||||
const parentChildRelations = new EnhancedMap<string, string[]>();
|
||||
const viaMap = new EnhancedMap<string, Set<string>>();
|
||||
data.events.map((ev: ISpaceSummaryEvent) => {
|
||||
if (ev.type === EventType.SpaceChild) {
|
||||
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
|
||||
}
|
||||
if (Array.isArray(ev.content["via"])) {
|
||||
const set = viaMap.getOrCreate(ev.state_key, new Set());
|
||||
ev.content["via"].forEach(via => set.add(via));
|
||||
}
|
||||
});
|
||||
|
||||
return [data.rooms, parentChildRelations, viaMap];
|
||||
} catch (e) {
|
||||
console.error(e); // TODO
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [space], []);
|
||||
const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space);
|
||||
|
||||
const roomsMap = useMemo(() => {
|
||||
if (!rooms) return null;
|
||||
const lcQuery = query.toLowerCase();
|
||||
const lcQuery = query.toLowerCase().trim();
|
||||
|
||||
const filteredRooms = rooms.filter(r => {
|
||||
return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms
|
||||
|| r.name?.toLowerCase().includes(lcQuery)
|
||||
|| r.topic?.toLowerCase().includes(lcQuery);
|
||||
const roomsMap = new Map<string, ISpaceSummaryRoom>(rooms.map(r => [r.room_id, r]));
|
||||
if (!lcQuery) return roomsMap;
|
||||
|
||||
const directMatches = rooms.filter(r => {
|
||||
return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery);
|
||||
});
|
||||
|
||||
return new Map<string, ISpaceSummaryRoom>(filteredRooms.map(r => [r.room_id, r]));
|
||||
// const root = rooms.get(space.roomId);
|
||||
}, [rooms, query]);
|
||||
// Walk back up the tree to find all parents of the direct matches to show their place in the hierarchy
|
||||
const visited = new Set<string>();
|
||||
const queue = [...directMatches.map(r => r.room_id)];
|
||||
while (queue.length) {
|
||||
const roomId = queue.pop();
|
||||
visited.add(roomId);
|
||||
childParentMap.get(roomId)?.forEach(parentId => {
|
||||
if (!visited.has(parentId)) {
|
||||
queue.push(parentId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove any mappings for rooms which were not visited in the walk
|
||||
Array.from(roomsMap.keys()).forEach(roomId => {
|
||||
if (!visited.has(roomId)) {
|
||||
roomsMap.delete(roomId);
|
||||
}
|
||||
});
|
||||
return roomsMap;
|
||||
}, [rooms, childParentMap, query]);
|
||||
|
||||
const title = <React.Fragment>
|
||||
<RoomAvatar room={space} height={40} width={40} />
|
||||
<RoomAvatar room={space} height={32} width={32} />
|
||||
<div>
|
||||
<h1>{ _t("Explore rooms") }</h1>
|
||||
<div><RoomName room={space} /></div>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
const explanation =
|
||||
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
|
||||
_t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.", null,
|
||||
{a: sub => {
|
||||
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
|
||||
}},
|
||||
);
|
||||
|
||||
const [error, setError] = useState("");
|
||||
const [removing, setRemoving] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
let content;
|
||||
if (roomsMap) {
|
||||
content = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
|
||||
<HierarchyLevel
|
||||
spaceId={space.roomId}
|
||||
rooms={roomsMap}
|
||||
editing={isEditing}
|
||||
relations={relations}
|
||||
parents={new Set()}
|
||||
queueAction={action => {
|
||||
pendingActions.current.set(action.event.room_id, action);
|
||||
}}
|
||||
onPreviewClick={roomId => {
|
||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false);
|
||||
onFinished();
|
||||
}}
|
||||
onJoinClick={(roomId) => {
|
||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true);
|
||||
onFinished();
|
||||
}}
|
||||
/>
|
||||
</AutoHideScrollbar>;
|
||||
const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
|
||||
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
||||
|
||||
let countsStr;
|
||||
if (numSpaces > 1) {
|
||||
countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces });
|
||||
} else if (numSpaces > 0) {
|
||||
countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces });
|
||||
} else {
|
||||
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||
}
|
||||
|
||||
let editSection;
|
||||
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
||||
});
|
||||
|
||||
let buttons;
|
||||
if (selectedRelations.length) {
|
||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||
});
|
||||
|
||||
const disabled = removing || saving;
|
||||
|
||||
buttons = <>
|
||||
<AccessibleButton
|
||||
onClick={async () => {
|
||||
setRemoving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
parentChildMap.get(parentId).get(childId).content = {};
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError(_t("Failed to remove some rooms. Try again later"));
|
||||
}
|
||||
setRemoving(false);
|
||||
}}
|
||||
kind="danger_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ removing ? _t("Removing...") : _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
const suggested = !selectionAllSuggested;
|
||||
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
|
||||
if (!existingContent || existingContent.suggested === suggested) continue;
|
||||
|
||||
const content = {
|
||||
...existingContent,
|
||||
suggested: !selectionAllSuggested,
|
||||
};
|
||||
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
|
||||
|
||||
parentChildMap.get(parentId).get(childId).content = content;
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Failed to update some suggestions. Try again later");
|
||||
}
|
||||
setSaving(false);
|
||||
}}
|
||||
kind="primary_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ saving
|
||||
? _t("Saving...")
|
||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||
}
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
editSection = <span>
|
||||
{ buttons }
|
||||
</span>;
|
||||
}
|
||||
|
||||
let results;
|
||||
if (roomsMap.size) {
|
||||
results = <>
|
||||
<HierarchyLevel
|
||||
spaceId={space.roomId}
|
||||
rooms={roomsMap}
|
||||
relations={parentChildMap}
|
||||
parents={new Set()}
|
||||
selectedMap={selected}
|
||||
onToggleClick={(parentId, childId) => {
|
||||
setError("");
|
||||
if (!selected.has(parentId)) {
|
||||
setSelected(new Map(selected.set(parentId, new Set([childId]))));
|
||||
return;
|
||||
}
|
||||
|
||||
const parentSet = selected.get(parentId);
|
||||
if (!parentSet.has(childId)) {
|
||||
setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId]))));
|
||||
return;
|
||||
}
|
||||
|
||||
parentSet.delete(childId);
|
||||
setSelected(new Map(selected.set(parentId, new Set(parentSet))));
|
||||
}}
|
||||
onViewRoomClick={(roomId, autoJoin) => {
|
||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
|
||||
onFinished();
|
||||
}}
|
||||
/>
|
||||
<hr />
|
||||
</>;
|
||||
} else {
|
||||
results = <div className="mx_SpaceRoomDirectory_noResults">
|
||||
<h3>{ _t("No results found") }</h3>
|
||||
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
content = <>
|
||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||
{ countsStr }
|
||||
{ editSection }
|
||||
</div>
|
||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||
{ error }
|
||||
</div> }
|
||||
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
|
||||
{ results }
|
||||
<AccessibleButton
|
||||
onClick={onCreateRoomClick}
|
||||
kind="primary"
|
||||
className="mx_SpaceRoomDirectory_createRoom"
|
||||
>
|
||||
{ _t("Create room") }
|
||||
</AccessibleButton>
|
||||
</AutoHideScrollbar>
|
||||
</>;
|
||||
} else {
|
||||
content = <Spinner />;
|
||||
}
|
||||
|
||||
// TODO loading state/error state
|
||||
|
@ -555,13 +571,12 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
|
|||
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Find a room...") }
|
||||
placeholder={ _t("Search names and description") }
|
||||
onSearch={setQuery}
|
||||
autoFocus={true}
|
||||
initialValue={initialText}
|
||||
/>
|
||||
|
||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||
{ adminButton }
|
||||
</div>
|
||||
{ content }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {RefObject, useContext, useRef, useState} from "react";
|
||||
import React, {RefObject, useContext, useMemo, useRef, useState} from "react";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {EventSubscription} from "fbemitter";
|
||||
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||
|
@ -25,13 +26,11 @@ import AccessibleButton from "../views/elements/AccessibleButton";
|
|||
import RoomName from "../views/elements/RoomName";
|
||||
import RoomTopic from "../views/elements/RoomTopic";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import FormButton from "../views/elements/FormButton";
|
||||
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
||||
import createRoom, {IOpts, Preset} from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||
import StyledRadioGroup from "../views/elements/StyledRadioGroup";
|
||||
import withValidation from "../views/elements/Validation";
|
||||
import * as Email from "../../email";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
@ -42,18 +41,16 @@ import ErrorBoundary from "../views/elements/ErrorBoundary";
|
|||
import {ActionPayload} from "../../dispatcher/payloads";
|
||||
import RightPanel from "./RightPanel";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||
import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {useStateArray} from "../../hooks/useStateArray";
|
||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
||||
import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
|
||||
import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom} from "./SpaceRoomDirectory";
|
||||
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
||||
import {EnhancedMap} from "../../utils/maps";
|
||||
import {HierarchyLevel, ISpaceSummaryRoom, showRoom, useSpaceSummary} from "./SpaceRoomDirectory";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -66,6 +63,7 @@ interface IProps {
|
|||
interface IState {
|
||||
phase: Phase;
|
||||
showRightPanel: boolean;
|
||||
myMembership: string;
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
|
@ -98,6 +96,8 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
const cli = useContext(MatrixClientContext);
|
||||
const myMembership = useMyRoomMembership(space);
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
let inviterSection;
|
||||
let joinButtons;
|
||||
if (myMembership === "invite") {
|
||||
|
@ -121,11 +121,41 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
}
|
||||
|
||||
joinButtons = <>
|
||||
<FormButton label={_t("Reject")} kind="secondary" onClick={onRejectButtonClicked} />
|
||||
<FormButton label={_t("Accept")} onClick={onJoinButtonClicked} />
|
||||
<AccessibleButton
|
||||
kind="secondary"
|
||||
onClick={() => {
|
||||
setBusy(true);
|
||||
onRejectButtonClicked();
|
||||
}}
|
||||
>
|
||||
{ _t("Reject") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
setBusy(true);
|
||||
onJoinButtonClicked();
|
||||
}}
|
||||
>
|
||||
{ _t("Accept") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
} else {
|
||||
joinButtons = <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
|
||||
joinButtons = (
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
setBusy(true);
|
||||
onJoinButtonClicked();
|
||||
}}
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
)
|
||||
}
|
||||
|
||||
if (busy) {
|
||||
joinButtons = <InlineSpinner />;
|
||||
}
|
||||
|
||||
let visibilitySection;
|
||||
|
@ -196,7 +226,7 @@ const SpaceLanding = ({ space }) => {
|
|||
|
||||
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
|
||||
|
||||
const [_, forceUpdate] = useStateToggle(false); // TODO
|
||||
const [refreshToken, forceUpdate] = useStateToggle(false);
|
||||
|
||||
let addRoomButtons;
|
||||
if (canAddRooms) {
|
||||
|
@ -226,26 +256,13 @@ const SpaceLanding = ({ space }) => {
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const [loading, roomsMap, relations, numRooms] = useAsyncMemo(async () => {
|
||||
try {
|
||||
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join");
|
||||
|
||||
const parentChildRelations = new EnhancedMap<string, string[]>();
|
||||
data.events.map((ev: ISpaceSummaryEvent) => {
|
||||
if (ev.type === EventType.SpaceChild) {
|
||||
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
|
||||
}
|
||||
});
|
||||
|
||||
const roomsMap = new Map<string, ISpaceSummaryRoom>(data.rooms.map(r => [r.room_id, r]));
|
||||
const numRooms = data.rooms.filter(r => r.room_type !== RoomType.Space).length;
|
||||
return [false, roomsMap, parentChildRelations, numRooms];
|
||||
} catch (e) {
|
||||
console.error(e); // TODO
|
||||
}
|
||||
|
||||
return [false];
|
||||
}, [space, _], [true]);
|
||||
const [rooms, relations, viaMap] = useSpaceSummary(cli, space, refreshToken);
|
||||
const [roomsMap, numRooms] = useMemo(() => {
|
||||
if (!rooms) return [];
|
||||
const roomsMap = new Map<string, ISpaceSummaryRoom>(rooms.map(r => [r.room_id, r]));
|
||||
const numRooms = rooms.filter(r => r.room_type !== RoomType.Space).length;
|
||||
return [roomsMap, numRooms];
|
||||
}, [rooms]);
|
||||
|
||||
let previewRooms;
|
||||
if (roomsMap) {
|
||||
|
@ -257,15 +274,14 @@ const SpaceLanding = ({ space }) => {
|
|||
<HierarchyLevel
|
||||
spaceId={space.roomId}
|
||||
rooms={roomsMap}
|
||||
editing={false}
|
||||
relations={relations}
|
||||
parents={new Set()}
|
||||
onPreviewClick={roomId => {
|
||||
showRoom(roomsMap.get(roomId), [], false); // TODO
|
||||
onViewRoomClick={(roomId, autoJoin) => {
|
||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
|
||||
}}
|
||||
/>
|
||||
</AutoHideScrollbar>;
|
||||
} else if (loading) {
|
||||
} else if (!rooms) {
|
||||
previewRooms = <InlineSpinner />;
|
||||
} else {
|
||||
previewRooms = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
|
||||
|
@ -337,6 +353,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||
placeholder={placeholders[i]}
|
||||
value={roomNames[i]}
|
||||
onChange={ev => setRoomName(i, ev.target.value)}
|
||||
autoFocus={i === 2}
|
||||
/>;
|
||||
});
|
||||
|
||||
|
@ -369,7 +386,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||
let buttonLabel = _t("Skip for now");
|
||||
if (roomNames.some(name => name.trim())) {
|
||||
onClick = onNextClick;
|
||||
buttonLabel = busy ? _t("Creating rooms...") : _t("Next")
|
||||
buttonLabel = busy ? _t("Creating rooms...") : _t("Continue")
|
||||
}
|
||||
|
||||
return <div>
|
||||
|
@ -380,61 +397,55 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
|||
{ fields }
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<FormButton
|
||||
label={buttonLabel}
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={busy}
|
||||
onClick={onClick}
|
||||
/>
|
||||
>
|
||||
{ buttonLabel }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceSetupPublicShare = ({ space, onFinished }) => {
|
||||
return <div className="mx_SpaceRoomView_publicShare">
|
||||
<h1>{ _t("Share your public space") }</h1>
|
||||
<div className="mx_SpacePublicShare_description">{ _t("At the moment only you can see it.") }</div>
|
||||
<h1>{ _t("Share %(name)s", { name: space.name }) }</h1>
|
||||
<div className="mx_SpaceRoomView_description">
|
||||
{ _t("It's just you at the moment, it will be even better with others.") }
|
||||
</div>
|
||||
|
||||
<SpacePublicShare space={space} onFinished={onFinished} />
|
||||
<SpacePublicShare space={space} />
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<FormButton label={_t("Finish")} onClick={onFinished} />
|
||||
<AccessibleButton kind="primary" onClick={onFinished}>
|
||||
{ _t("Go to my first room") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceSetupPrivateScope = ({ onFinished }) => {
|
||||
const [option, setOption] = useState<string>(null);
|
||||
|
||||
const SpaceSetupPrivateScope = ({ space, onFinished }) => {
|
||||
return <div className="mx_SpaceRoomView_privateScope">
|
||||
<h1>{ _t("Who are you working with?") }</h1>
|
||||
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
|
||||
|
||||
<StyledRadioGroup
|
||||
name="privateSpaceScope"
|
||||
value={option}
|
||||
onChange={setOption}
|
||||
definitions={[
|
||||
{
|
||||
value: "justMe",
|
||||
className: "mx_SpaceRoomView_privateScope_justMeButton",
|
||||
label: <React.Fragment>
|
||||
<h3>{ _t("Just Me") }</h3>
|
||||
<div>{ _t("A private space just for you") }</div>
|
||||
</React.Fragment>,
|
||||
}, {
|
||||
value: "meAndMyTeammates",
|
||||
className: "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton",
|
||||
label: <React.Fragment>
|
||||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
</React.Fragment>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<FormButton label={_t("Next")} disabled={!option} onClick={() => onFinished(option !== "justMe")} />
|
||||
<div className="mx_SpaceRoomView_description">
|
||||
{ _t("Make sure the right people have access to %(name)s", { name: space.name }) }
|
||||
</div>
|
||||
|
||||
<AccessibleButton
|
||||
className="mx_SpaceRoomView_privateScope_justMeButton"
|
||||
onClick={() => { onFinished(false) }}
|
||||
>
|
||||
<h3>{ _t("Just me") }</h3>
|
||||
<div>{ _t("A private space to organise your rooms") }</div>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_SpaceRoomView_privateScope_meAndMyTeammatesButton"
|
||||
onClick={() => { onFinished(true) }}
|
||||
>
|
||||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -464,6 +475,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
|||
onChange={ev => setEmailAddress(i, ev.target.value)}
|
||||
ref={fieldRefs[i]}
|
||||
onValidate={validateEmailRules}
|
||||
autoFocus={i === 0}
|
||||
/>;
|
||||
});
|
||||
|
||||
|
@ -501,9 +513,18 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
|||
setBusy(false);
|
||||
};
|
||||
|
||||
let onClick = onFinished;
|
||||
let buttonLabel = _t("Skip for now");
|
||||
if (emailAddresses.some(name => name.trim())) {
|
||||
onClick = onNextClick;
|
||||
buttonLabel = busy ? _t("Inviting...") : _t("Continue")
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_inviteTeammates">
|
||||
<h1>{ _t("Invite your teammates") }</h1>
|
||||
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
|
||||
<div className="mx_SpaceRoomView_description">
|
||||
{ _t("Make sure the right people have access. You can invite more later.") }
|
||||
</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
{ fields }
|
||||
|
@ -518,8 +539,9 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
|||
</div>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<AccessibleButton onClick={onFinished} kind="link">{_t("Skip for now")}</AccessibleButton>
|
||||
<FormButton label={busy ? _t("Inviting...") : _t("Next")} disabled={busy} onClick={onNextClick} />
|
||||
<AccessibleButton kind="primary" disabled={busy} onClick={onClick}>
|
||||
{ buttonLabel }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
@ -547,17 +569,26 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
this.state = {
|
||||
phase,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
myMembership: this.props.space.getMyMembership(),
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
||||
this.context.on("Room.myMembership", this.onMyMembership);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.rightPanelStoreToken.remove();
|
||||
this.context.off("Room.myMembership", this.onMyMembership);
|
||||
}
|
||||
|
||||
private onMyMembership = (room: Room, myMembership: string) => {
|
||||
if (room.roomId === this.props.space.roomId) {
|
||||
this.setState({ myMembership });
|
||||
}
|
||||
};
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
|
@ -594,10 +625,45 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private goToFirstRoom = async () => {
|
||||
// TODO actually go to the first room
|
||||
|
||||
const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId);
|
||||
if (childRooms.length) {
|
||||
const room = childRooms[0];
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let suggestedRooms = SpaceStore.instance.suggestedRooms;
|
||||
if (SpaceStore.instance.activeSpace !== this.props.space) {
|
||||
// the space store has the suggested rooms loaded for a different space, fetch the right ones
|
||||
suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)).rooms;
|
||||
}
|
||||
|
||||
if (suggestedRooms.length) {
|
||||
const room = suggestedRooms[0];
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: room.room_id,
|
||||
oobData: {
|
||||
avatarUrl: room.avatar_url,
|
||||
name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ phase: Phase.Landing });
|
||||
};
|
||||
|
||||
private renderBody() {
|
||||
switch (this.state.phase) {
|
||||
case Phase.Landing:
|
||||
if (this.props.space.getMyMembership() === "join") {
|
||||
if (this.state.myMembership === "join") {
|
||||
return <SpaceLanding space={this.props.space} />;
|
||||
} else {
|
||||
return <SpacePreview
|
||||
|
@ -610,17 +676,16 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
return <SpaceSetupFirstRooms
|
||||
space={this.props.space}
|
||||
title={_t("What are some things you want to discuss?")}
|
||||
description={_t("We'll create rooms for each topic.")}
|
||||
description={_t("Let's create a room for each of them. " +
|
||||
"You can add more later too, including already existing ones.")}
|
||||
onFinished={() => this.setState({ phase: Phase.PublicShare })}
|
||||
/>;
|
||||
case Phase.PublicShare:
|
||||
return <SpaceSetupPublicShare
|
||||
space={this.props.space}
|
||||
onFinished={() => this.setState({ phase: Phase.Landing })}
|
||||
/>;
|
||||
return <SpaceSetupPublicShare space={this.props.space} onFinished={this.goToFirstRoom} />;
|
||||
|
||||
case Phase.PrivateScope:
|
||||
return <SpaceSetupPrivateScope
|
||||
space={this.props.space}
|
||||
onFinished={(invite: boolean) => {
|
||||
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms });
|
||||
}}
|
||||
|
@ -634,7 +699,8 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
return <SpaceSetupFirstRooms
|
||||
space={this.props.space}
|
||||
title={_t("What projects are you working on?")}
|
||||
description={_t("We'll create rooms for each of them. You can add existing rooms after setup.")}
|
||||
description={_t("We'll create rooms for each of them. " +
|
||||
"You can add more later too, including already existing ones.")}
|
||||
onFinished={() => this.setState({ phase: Phase.Landing })}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@ import {LayoutPropType} from "../../settings/Layout";
|
|||
import React, {createRef} from 'react';
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
import {EventTimeline} from "matrix-js-sdk";
|
||||
import * as Matrix from "matrix-js-sdk";
|
||||
import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
|
||||
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
|
||||
import { _t } from '../../languageHandler';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import UserActivity from "../../UserActivity";
|
||||
|
@ -463,6 +463,9 @@ class TimelinePanel extends React.Component {
|
|||
}
|
||||
});
|
||||
}
|
||||
if (payload.action === "scroll_to_bottom") {
|
||||
this.jumpToLiveTimeline();
|
||||
}
|
||||
};
|
||||
|
||||
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
|
||||
|
@ -1007,7 +1010,7 @@ class TimelinePanel extends React.Component {
|
|||
* returns a promise which will resolve when the load completes.
|
||||
*/
|
||||
_loadTimeline(eventId, pixelOffset, offsetBase) {
|
||||
this._timelineWindow = new Matrix.TimelineWindow(
|
||||
this._timelineWindow = new TimelineWindow(
|
||||
MatrixClientPeg.get(), this.props.timelineSet,
|
||||
{windowLimit: this.props.timelineCap});
|
||||
|
||||
|
|
|
@ -17,13 +17,14 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Matrix from "matrix-js-sdk";
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import * as sdk from "../../index";
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
import HomePage from "./HomePage";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
@replaceableComponent("structures.UserView")
|
||||
export default class UserView extends React.Component {
|
||||
|
@ -68,8 +69,8 @@ export default class UserView extends React.Component {
|
|||
this.setState({loading: false});
|
||||
return;
|
||||
}
|
||||
const fakeEvent = new Matrix.MatrixEvent({type: "m.room.member", content: profileInfo});
|
||||
const member = new Matrix.RoomMember(null, this.props.userId);
|
||||
const fakeEvent = new MatrixEvent({type: "m.room.member", content: profileInfo});
|
||||
const member = new RoomMember(null, this.props.userId);
|
||||
member.setMembershipEvent(fakeEvent);
|
||||
this.setState({member, loading: false});
|
||||
}
|
||||
|
|
|
@ -16,65 +16,176 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SyntaxHighlight from '../views/elements/SyntaxHighlight';
|
||||
import {_t} from "../../languageHandler";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
|
||||
import { _t } from "../../languageHandler";
|
||||
import * as sdk from "../../index";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
|
||||
import { canEditContent } from "../../utils/EventUtils";
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("structures.ViewSource")
|
||||
export default class ViewSource extends React.Component {
|
||||
static propTypes = {
|
||||
content: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
roomId: PropTypes.string.isRequired,
|
||||
eventId: PropTypes.string.isRequired,
|
||||
isEncrypted: PropTypes.bool.isRequired,
|
||||
decryptedContent: PropTypes.object,
|
||||
mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let content;
|
||||
if (this.props.isEncrypted) {
|
||||
content = <>
|
||||
<details open className="mx_ViewSource_details">
|
||||
<summary>
|
||||
<span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
|
||||
</summary>
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.props.decryptedContent, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
</details>
|
||||
<details className="mx_ViewSource_details">
|
||||
<summary>
|
||||
<span className="mx_ViewSource_heading">{_t("Original event source")}</span>
|
||||
</summary>
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.props.content, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
</details>
|
||||
</>;
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
};
|
||||
}
|
||||
|
||||
onBack() {
|
||||
// TODO: refresh the "Event ID:" modal header
|
||||
this.setState({ isEditing: false });
|
||||
}
|
||||
|
||||
onEdit() {
|
||||
this.setState({ isEditing: true });
|
||||
}
|
||||
|
||||
// returns the dialog body for viewing the event source
|
||||
viewSourceContent() {
|
||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||
const isEncrypted = mxEvent.isEncrypted();
|
||||
const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private
|
||||
const originalEventSource = mxEvent.event;
|
||||
|
||||
if (isEncrypted) {
|
||||
return (
|
||||
<>
|
||||
<details open className="mx_ViewSource_details">
|
||||
<summary>
|
||||
<span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
|
||||
</summary>
|
||||
<SyntaxHighlight className="json">{JSON.stringify(decryptedEventSource, null, 2)}</SyntaxHighlight>
|
||||
</details>
|
||||
<details className="mx_ViewSource_details">
|
||||
<summary>
|
||||
<span className="mx_ViewSource_heading">{_t("Original event source")}</span>
|
||||
</summary>
|
||||
<SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
|
||||
</details>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
content = <>
|
||||
<div className="mx_ViewSource_heading">{_t("Original event source")}</div>
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.props.content, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<div className="mx_ViewSource_heading">{_t("Original event source")}</div>
|
||||
<SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// returns the id of the initial message, not the id of the previous edit
|
||||
getBaseEventId() {
|
||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||
const isEncrypted = mxEvent.isEncrypted();
|
||||
const baseMxEvent = this.props.mxEvent;
|
||||
|
||||
if (isEncrypted) {
|
||||
// `relates_to` field is inside the encrypted event
|
||||
return mxEvent.event.content["m.relates_to"]?.event_id ?? baseMxEvent.getId();
|
||||
} else {
|
||||
return mxEvent.getContent()["m.relates_to"]?.event_id ?? baseMxEvent.getId();
|
||||
}
|
||||
}
|
||||
|
||||
// returns the SendCustomEvent component prefilled with the correct details
|
||||
editSourceContent() {
|
||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||
|
||||
const isStateEvent = mxEvent.isState();
|
||||
const roomId = mxEvent.getRoomId();
|
||||
const originalContent = mxEvent.getContent();
|
||||
|
||||
if (isStateEvent) {
|
||||
return (
|
||||
<MatrixClientContext.Consumer>
|
||||
{(cli) => (
|
||||
<SendCustomEvent
|
||||
room={cli.getRoom(roomId)}
|
||||
forceStateEvent={true}
|
||||
onBack={() => this.onBack()}
|
||||
inputs={{
|
||||
eventType: mxEvent.getType(),
|
||||
evContent: JSON.stringify(originalContent, null, "\t"),
|
||||
stateKey: mxEvent.getStateKey(),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MatrixClientContext.Consumer>
|
||||
);
|
||||
} else {
|
||||
// prefill an edit-message event
|
||||
// keep only the `body` and `msgtype` fields of originalContent
|
||||
const bodyToStartFrom = originalContent["m.new_content"]?.body ?? originalContent.body; // prefill the last edit body, to start editing from there
|
||||
const newContent = {
|
||||
"body": ` * ${bodyToStartFrom}`,
|
||||
"msgtype": originalContent.msgtype,
|
||||
"m.new_content": {
|
||||
body: bodyToStartFrom,
|
||||
msgtype: originalContent.msgtype,
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: "m.replace",
|
||||
event_id: this.getBaseEventId(),
|
||||
},
|
||||
};
|
||||
return (
|
||||
<MatrixClientContext.Consumer>
|
||||
{(cli) => (
|
||||
<SendCustomEvent
|
||||
room={cli.getRoom(roomId)}
|
||||
forceStateEvent={false}
|
||||
forceGeneralEvent={true}
|
||||
onBack={() => this.onBack()}
|
||||
inputs={{
|
||||
eventType: mxEvent.getType(),
|
||||
evContent: JSON.stringify(newContent, null, "\t"),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MatrixClientContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
canSendStateEvent(mxEvent) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(mxEvent.getRoomId());
|
||||
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||
|
||||
const isEditing = this.state.isEditing;
|
||||
const roomId = mxEvent.getRoomId();
|
||||
const eventId = mxEvent.getId();
|
||||
const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent);
|
||||
return (
|
||||
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_ViewSource_label_left">Event ID: { this.props.eventId }</div>
|
||||
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
|
||||
<div>
|
||||
<div>Room ID: {roomId}</div>
|
||||
<div>Event ID: {eventId}</div>
|
||||
<div className="mx_ViewSource_separator" />
|
||||
{ content }
|
||||
{isEditing ? this.editSourceContent() : this.viewSourceContent()}
|
||||
</div>
|
||||
{!isEditing && canEdit && (
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={() => this.onEdit()}>{_t("Edit")}</button>
|
||||
</div>
|
||||
)}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import {createClient} from 'matrix-js-sdk/src/matrix';
|
||||
import React, {ReactNode} from 'react';
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
||||
|
@ -181,7 +181,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const {hsUrl, isUrl} = serverConfig;
|
||||
const cli = Matrix.createClient({
|
||||
const cli = createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ import AccessibleButton from '../elements/AccessibleButton';
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
|
||||
interface IProps {
|
||||
name: string; // The name (first initial used as default)
|
||||
|
@ -35,7 +36,7 @@ interface IProps {
|
|||
width?: number;
|
||||
height?: number;
|
||||
// XXX: resizeMethod not actually used.
|
||||
resizeMethod?: string;
|
||||
resizeMethod?: ResizeMethod;
|
||||
defaultToInitialLetter?: boolean; // true to add default url
|
||||
onClick?: React.MouseEventHandler;
|
||||
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 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.
|
||||
|
@ -15,9 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
|
||||
export interface IProps {
|
||||
groupId?: string;
|
||||
|
@ -25,7 +26,7 @@ export interface IProps {
|
|||
groupAvatarUrl?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: string;
|
||||
resizeMethod?: ResizeMethod;
|
||||
onClick?: React.MouseEventHandler;
|
||||
}
|
||||
|
||||
|
@ -38,8 +39,8 @@ export default class GroupAvatar extends React.Component<IProps> {
|
|||
};
|
||||
|
||||
getGroupAvatarUrl() {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(
|
||||
this.props.groupAvatarUrl,
|
||||
if (!this.props.groupAvatarUrl) return null;
|
||||
return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp(
|
||||
this.props.width,
|
||||
this.props.height,
|
||||
this.props.resizeMethod,
|
||||
|
|
|
@ -20,16 +20,17 @@ import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
|||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
|
||||
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
||||
member: RoomMember;
|
||||
fallbackUserId?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
resizeMethod?: string;
|
||||
resizeMethod?: ResizeMethod;
|
||||
// The onClick to give the avatar
|
||||
onClick?: React.MouseEventHandler;
|
||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||
|
@ -63,18 +64,19 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private static getState(props: IProps): IState {
|
||||
if (props.member && props.member.name) {
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: props.member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
if (props.member?.name) {
|
||||
let imageUrl = null;
|
||||
if (props.member.getMxcAvatarUrl()) {
|
||||
imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: imageUrl,
|
||||
};
|
||||
} else if (props.fallbackUserId) {
|
||||
return {
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
import React, {ComponentProps} from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import ImageView from '../elements/ImageView';
|
||||
|
@ -24,6 +23,7 @@ import Modal from '../../../Modal';
|
|||
import * as Avatar from '../../../Avatar';
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
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,
|
||||
|
@ -90,16 +90,16 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private static getImageUrls(props: IProps): string[] {
|
||||
return [
|
||||
getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
// Default props don't play nicely with getDerivedStateFromProps
|
||||
//props.oobData !== undefined ? props.oobData.avatarUrl : {},
|
||||
props.oobData.avatarUrl,
|
||||
let oobAvatar = null;
|
||||
if (props.oobData.avatarUrl) {
|
||||
oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp(
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
), // highest priority
|
||||
);
|
||||
}
|
||||
return [
|
||||
oobAvatar, // highest priority
|
||||
RoomAvatar.getRoomAvatarUrl(props),
|
||||
].filter(function(url) {
|
||||
return (url !== null && url !== "");
|
||||
|
|
|
@ -14,21 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {ComponentProps, useContext} from 'react';
|
||||
import React, {ComponentProps} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {IApp} from "../../../stores/WidgetStore";
|
||||
import BaseAvatar, {BaseAvatarType} from "./BaseAvatar";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls"> {
|
||||
app: IApp;
|
||||
}
|
||||
|
||||
const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 20, ...props }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
|
||||
// heuristics for some better icons until Widgets support their own icons
|
||||
if (app.type.includes("jitsi")) {
|
||||
|
@ -47,7 +44,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
|
|||
name={app.id}
|
||||
className={classNames("mx_WidgetAvatar", className)}
|
||||
// MSC2765
|
||||
url={app.avatar_url ? getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop") : undefined}
|
||||
url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : undefined}
|
||||
urls={iconUrls}
|
||||
width={width}
|
||||
height={height}
|
||||
|
|
|
@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
|
|||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import {Group} from 'matrix-js-sdk';
|
||||
import {Group} from 'matrix-js-sdk/src/models/group';
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -126,15 +126,9 @@ export default class MessageContextMenu extends React.Component {
|
|||
};
|
||||
|
||||
onViewSourceClick = () => {
|
||||
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
||||
roomId: ev.getRoomId(),
|
||||
eventId: ev.getId(),
|
||||
content: ev.event,
|
||||
isEncrypted: ev.isEncrypted(),
|
||||
// FIXME: _clearEvent is private
|
||||
decryptedContent: ev._clearEvent,
|
||||
mxEvent: this.props.mxEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
this.closeMenu();
|
||||
};
|
||||
|
|
|
@ -22,7 +22,6 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
|
|||
import {_t} from '../../../languageHandler';
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import FormButton from "../elements/FormButton";
|
||||
import Dropdown from "../elements/Dropdown";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
@ -69,6 +68,7 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
const existingRoomsSet = new Set(existingRooms);
|
||||
const rooms = cli.getVisibleRooms().filter(room => {
|
||||
return !existingRoomsSet.has(room) // not already in space
|
||||
&& !room.isSpaceRoom() // not a space itself
|
||||
&& room.name.toLowerCase().includes(lcQuery) // contains query
|
||||
&& !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
|
||||
});
|
||||
|
@ -109,7 +109,7 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
const title = <React.Fragment>
|
||||
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
||||
<div>
|
||||
<h1>{ _t("Add existing spaces/rooms") }</h1>
|
||||
<h1>{ _t("Add existing rooms") }</h1>
|
||||
{ spaceOptionSection }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
@ -127,29 +127,9 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Filter your rooms and spaces") }
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog">
|
||||
{ spaces.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selectedToAdd.has(space)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(space);
|
||||
} else {
|
||||
selectedToAdd.delete(space);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpaceDialog_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
|
@ -171,6 +151,27 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
</div>
|
||||
) : undefined }
|
||||
|
||||
{ spaces.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selectedToAdd.has(space)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(space);
|
||||
} else {
|
||||
selectedToAdd.delete(space);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
|
@ -184,8 +185,8 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
</AccessibleButton>
|
||||
</span>
|
||||
|
||||
<FormButton
|
||||
label={busy ? _t("Applying...") : _t("Apply")}
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={busy || selectedToAdd.size < 1}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
|
@ -199,7 +200,9 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
|||
}
|
||||
setBusy(false);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{ busy ? _t("Adding...") : _t("Add") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
|
|
@ -26,12 +26,12 @@ import SdkConfig from "../../../SdkConfig";
|
|||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import InviteDialog from "./InviteDialog";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "./ErrorDialog";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
roomId: string;
|
||||
|
@ -142,12 +142,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
|
|||
|
||||
private renderPerson(person: IPerson, key: any) {
|
||||
const avatarSize = 36;
|
||||
let avatarUrl = null;
|
||||
if (person.user.getMxcAvatarUrl()) {
|
||||
avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize);
|
||||
}
|
||||
return (
|
||||
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
|
||||
<BaseAvatar
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), person.user.getMxcAvatarUrl(),
|
||||
avatarSize, avatarSize, "crop")}
|
||||
url={avatarUrl}
|
||||
name={person.user.name}
|
||||
idName={person.user.userId}
|
||||
width={avatarSize}
|
||||
|
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
/*
|
||||
* A dialog for confirming an operation on another user.
|
||||
|
@ -108,8 +109,9 @@ export default class ConfirmUserActionDialog extends React.Component {
|
|||
name = this.props.member.name;
|
||||
userId = this.props.member.userId;
|
||||
} else {
|
||||
const httpAvatarUrl = this.props.groupMember.avatarUrl ?
|
||||
this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
|
||||
const httpAvatarUrl = this.props.groupMember.avatarUrl
|
||||
? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48)
|
||||
: null;
|
||||
name = this.props.groupMember.displayname || this.props.groupMember.userId;
|
||||
userId = this.props.groupMember.userId;
|
||||
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
|
||||
|
|
|
@ -19,7 +19,6 @@ import PropTypes from 'prop-types';
|
|||
import * as sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { Room, MatrixEvent } from "matrix-js-sdk";
|
||||
import Field from "../elements/Field";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
|
@ -39,6 +38,8 @@ import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
|
|||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "./ErrorDialog";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
|
@ -74,13 +75,14 @@ class GenericEditor extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class SendCustomEvent extends GenericEditor {
|
||||
export class SendCustomEvent extends GenericEditor {
|
||||
static getLabel() { return _t('Send Custom Event'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
forceStateEvent: PropTypes.bool,
|
||||
forceGeneralEvent: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -141,6 +143,8 @@ class SendCustomEvent extends GenericEditor {
|
|||
</div>;
|
||||
}
|
||||
|
||||
const showTglFlip = !this.state.message && !this.props.forceStateEvent && !this.props.forceGeneralEvent;
|
||||
|
||||
return <div>
|
||||
<div className="mx_DevTools_content">
|
||||
<div className="mx_DevTools_eventTypeStateKeyGroup">
|
||||
|
@ -156,7 +160,7 @@ class SendCustomEvent extends GenericEditor {
|
|||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
||||
{ !this.state.message && !this.props.forceStateEvent && <div style={{float: "right"}}>
|
||||
{ showTglFlip && <div style={{float: "right"}}>
|
||||
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
|
||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
|
||||
</div> }
|
||||
|
|
|
@ -24,6 +24,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
communityId: string;
|
||||
|
@ -118,7 +119,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
|
|||
let preview = <img src={this.state.avatarPreview} className="mx_EditCommunityPrototypeDialog_avatar" />;
|
||||
if (!this.state.avatarPreview) {
|
||||
if (this.state.currentAvatarUrl) {
|
||||
const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl);
|
||||
const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
|
||||
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
|
||||
} else {
|
||||
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />
|
||||
|
|
|
@ -100,6 +100,20 @@ export default (props) => {
|
|||
);
|
||||
}
|
||||
|
||||
let bugReports = null;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReports = (
|
||||
<p>{
|
||||
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
|
||||
"to help us track down the problem.", {}, {
|
||||
debugLogsLink: sub => (
|
||||
<AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{sub}</AccessibleButton>
|
||||
),
|
||||
})
|
||||
}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (<QuestionDialog
|
||||
className="mx_FeedbackDialog"
|
||||
hasCancelButton={!!hasFeedback}
|
||||
|
@ -120,14 +134,7 @@ export default (props) => {
|
|||
},
|
||||
})
|
||||
}</p>
|
||||
<p>{
|
||||
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
|
||||
"to help us track down the problem.", {}, {
|
||||
debugLogsLink: sub => (
|
||||
<AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{sub}</AccessibleButton>
|
||||
),
|
||||
})
|
||||
}</p>
|
||||
{bugReports}
|
||||
</div>
|
||||
{ countlyFeedbackSection }
|
||||
</React.Fragment>}
|
||||
|
|
|
@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
const PHASE_START = 0;
|
||||
const PHASE_SHOW_SAS = 1;
|
||||
|
@ -123,22 +124,21 @@ export default class IncomingSasDialog extends React.Component {
|
|||
const Spinner = sdk.getComponent("views.elements.Spinner");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
const isSelf = this.props.verifier.userId == MatrixClientPeg.get().getUserId();
|
||||
const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId();
|
||||
|
||||
let profile;
|
||||
if (this.state.opponentProfile) {
|
||||
const oppProfile = this.state.opponentProfile;
|
||||
if (oppProfile) {
|
||||
const url = oppProfile.avatar_url
|
||||
? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio))
|
||||
: null;
|
||||
profile = <div className="mx_IncomingSasDialog_opponentProfile">
|
||||
<BaseAvatar name={this.state.opponentProfile.displayname}
|
||||
<BaseAvatar name={oppProfile.displayname}
|
||||
idName={this.props.verifier.userId}
|
||||
url={MatrixClientPeg.get().mxcUrlToHttp(
|
||||
this.state.opponentProfile.avatar_url,
|
||||
Math.floor(48 * window.devicePixelRatio),
|
||||
Math.floor(48 * window.devicePixelRatio),
|
||||
'crop',
|
||||
)}
|
||||
url={url}
|
||||
width={48} height={48} resizeMethod='crop'
|
||||
/>
|
||||
<h2>{this.state.opponentProfile.displayname}</h2>
|
||||
<h2>{oppProfile.displayname}</h2>
|
||||
</div>;
|
||||
} else if (this.state.opponentProfileError) {
|
||||
profile = <div>
|
||||
|
|
|
@ -22,7 +22,6 @@ import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Pe
|
|||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import * as Email from "../../../email";
|
||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||
|
@ -43,6 +42,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics";
|
|||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -160,9 +160,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||
width={avatarSize} height={avatarSize} />
|
||||
: <BaseAvatar
|
||||
className='mx_InviteDialog_userTile_avatar'
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||
avatarSize, avatarSize, "crop")}
|
||||
url={this.props.member.getMxcAvatarUrl()
|
||||
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
|
||||
: null}
|
||||
name={this.props.member.name}
|
||||
idName={this.props.member.userId}
|
||||
width={avatarSize}
|
||||
|
@ -262,9 +262,9 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
width={avatarSize} height={avatarSize} />
|
||||
: <BaseAvatar
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||
avatarSize, avatarSize, "crop")}
|
||||
url={this.props.member.getMxcAvatarUrl()
|
||||
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
|
||||
: null}
|
||||
name={this.props.member.name}
|
||||
idName={this.props.member.userId}
|
||||
width={avatarSize}
|
||||
|
@ -673,7 +673,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
console.error(err);
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("We couldn't create your DM. Please check the users you want to invite and try again."),
|
||||
errorText: _t("We couldn't create your DM."),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -886,19 +886,21 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
};
|
||||
|
||||
_toggleMember = (member: Member) => {
|
||||
let filterText = this.state.filterText;
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) {
|
||||
targets.splice(idx, 1);
|
||||
} else {
|
||||
targets.push(member);
|
||||
filterText = ""; // clear the filter when the user accepts a suggestion
|
||||
}
|
||||
this.setState({targets, filterText});
|
||||
if (!this.state.busy) {
|
||||
let filterText = this.state.filterText;
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) {
|
||||
targets.splice(idx, 1);
|
||||
} else {
|
||||
targets.push(member);
|
||||
filterText = ""; // clear the filter when the user accepts a suggestion
|
||||
}
|
||||
this.setState({targets, filterText});
|
||||
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1256,7 +1258,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
? _t("Invite to %(spaceName)s", {
|
||||
spaceName: room.name || _t("Unnamed Space"),
|
||||
})
|
||||
: _t("Invite to this room");
|
||||
: _t("Invite to %(roomName)s", {
|
||||
roomName: room.name || _t("Unnamed Room"),
|
||||
});
|
||||
|
||||
let helpTextUntranslated;
|
||||
if (isSpace) {
|
||||
|
|
|
@ -18,7 +18,7 @@ import React, {PureComponent} from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Markdown from '../../../Markdown';
|
||||
|
|
|
@ -82,6 +82,33 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
|||
|
||||
const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
|
||||
|
||||
let bugReports = (
|
||||
<p>
|
||||
{_t(
|
||||
"This usually only affects how the room is processed on the server. If you're " +
|
||||
"having problems with your %(brand)s, please report a bug.", {brand},
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReports = (
|
||||
<p>
|
||||
{_t(
|
||||
"This usually only affects how the room is processed on the server. If you're " +
|
||||
"having problems with your %(brand)s, please <a>report a bug</a>.",
|
||||
{
|
||||
brand,
|
||||
},
|
||||
{
|
||||
"a": (sub) => {
|
||||
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
|
||||
},
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_RoomUpgradeWarningDialog'
|
||||
|
@ -97,20 +124,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
|||
"is unstable due to bugs, missing features or security vulnerabilities.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"This usually only affects how the room is processed on the server. If you're " +
|
||||
"having problems with your %(brand)s, please <a>report a bug</a>.",
|
||||
{
|
||||
brand,
|
||||
},
|
||||
{
|
||||
"a": (sub) => {
|
||||
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
|
||||
},
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
{bugReports}
|
||||
<p>
|
||||
{_t(
|
||||
"You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
||||
|
|
|
@ -28,7 +28,6 @@ import {getTopic} from "../elements/RoomTopic";
|
|||
import {avatarUrlForRoom} from "../../../Avatar";
|
||||
import ToggleSwitch from "../elements/ToggleSwitch";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import FormButton from "../elements/FormButton";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {allSettled} from "../../../utils/promise";
|
||||
|
@ -134,16 +133,17 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
/>
|
||||
</div>
|
||||
|
||||
<FormButton
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
label={_t("Leave Space")}
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{ _t("Leave Space") }
|
||||
</AccessibleButton>
|
||||
|
||||
<div className="mx_SpaceSettingsDialog_buttons">
|
||||
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
|
||||
|
@ -152,7 +152,9 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<FormButton onClick={onSave} disabled={busy} label={busy ? _t("Saving...") : _t("Save Changes")} />
|
||||
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
|
||||
{ busy ? _t("Saving...") : _t("Save Changes") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import {Room} from "matrix-js-sdk";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import * as sdk from '../../../index';
|
||||
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms";
|
||||
import classNames from 'classnames';
|
||||
|
|
|
@ -20,8 +20,8 @@ import PropTypes from 'prop-types';
|
|||
import * as sdk from '../../../index';
|
||||
import { _t, pickBestLanguage } from '../../../languageHandler';
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
|
||||
|
||||
class TermsCheckbox extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -85,22 +85,22 @@ export default class TermsDialog extends React.PureComponent {
|
|||
|
||||
_nameForServiceType(serviceType, host) {
|
||||
switch (serviceType) {
|
||||
case Matrix.SERVICE_TYPES.IS:
|
||||
case SERVICE_TYPES.IS:
|
||||
return <div>{_t("Identity Server")}<br />({host})</div>;
|
||||
case Matrix.SERVICE_TYPES.IM:
|
||||
case SERVICE_TYPES.IM:
|
||||
return <div>{_t("Integration Manager")}<br />({host})</div>;
|
||||
}
|
||||
}
|
||||
|
||||
_summaryForServiceType(serviceType) {
|
||||
switch (serviceType) {
|
||||
case Matrix.SERVICE_TYPES.IS:
|
||||
case SERVICE_TYPES.IS:
|
||||
return <div>
|
||||
{_t("Find others by phone or email")}
|
||||
<br />
|
||||
{_t("Be found by phone or email")}
|
||||
</div>;
|
||||
case Matrix.SERVICE_TYPES.IM:
|
||||
case SERVICE_TYPES.IM:
|
||||
return <div>
|
||||
{_t("Use bots, bridges, widgets and sticker packs")}
|
||||
</div>;
|
||||
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../../index';
|
||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../SecurityManager';
|
||||
|
||||
|
|
|
@ -19,10 +19,10 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { UserAddressType } from '../../../UserAddress.js';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.elements.AddressTile")
|
||||
export default class AddressTile extends React.Component {
|
||||
|
@ -47,9 +47,7 @@ export default class AddressTile extends React.Component {
|
|||
const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
|
||||
|
||||
if (isMatrixAddress && address.avatarMxc) {
|
||||
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
||||
address.avatarMxc, 25, 25, 'crop',
|
||||
));
|
||||
imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25));
|
||||
} else if (address.addressType === 'email') {
|
||||
imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
|
||||
}
|
||||
|
|
|
@ -70,9 +70,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
const client = MatrixClientPeg.get();
|
||||
const userId = client.getUserId();
|
||||
const profileInfo = await client.getProfileInfo(userId);
|
||||
const avatarUrl = Avatar.avatarUrlForUser(
|
||||
{avatarUrl: profileInfo.avatar_url},
|
||||
AVATAR_SIZE, AVATAR_SIZE, "crop");
|
||||
const avatarUrl = profileInfo.avatar_url;
|
||||
|
||||
this.setState({
|
||||
userId,
|
||||
|
@ -113,8 +111,9 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
name: displayname,
|
||||
userId: userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return avatarUrl;
|
||||
return Avatar.avatarUrlForUser({avatarUrl}, AVATAR_SIZE, AVATAR_SIZE, "crop");
|
||||
},
|
||||
getMxcAvatarUrl: () => avatarUrl,
|
||||
};
|
||||
|
||||
return event;
|
||||
|
|
|
@ -20,6 +20,7 @@ import FlairStore from '../../../stores/FlairStore';
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
|
||||
class FlairAvatar extends React.Component {
|
||||
|
@ -39,8 +40,7 @@ class FlairAvatar extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const httpUrl = this.context.mxcUrlToHttp(
|
||||
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
|
||||
const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16);
|
||||
const tooltip = this.props.groupProfile.name ?
|
||||
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
||||
this.props.groupProfile.groupId;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 - 2019, 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.
|
||||
|
@ -19,13 +17,15 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import Tooltip from './Tooltip';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
|
@ -254,12 +254,12 @@ class Pill extends React.Component {
|
|||
case Pill.TYPE_GROUP_MENTION: {
|
||||
if (this.state.group) {
|
||||
const {avatarUrl, groupId, name} = this.state.group;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
linkText = groupId;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <BaseAvatar name={name || groupId} width={16} height={16} aria-hidden="true"
|
||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
|
||||
avatar = <BaseAvatar
|
||||
name={name || groupId} width={16} height={16} aria-hidden="true"
|
||||
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
|
||||
}
|
||||
pillClass = 'mx_GroupPill';
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import {_t} from '../../../languageHandler';
|
|||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {wantsDateSeparator} from '../../../DateUtils';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
|
||||
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {LayoutPropType} from "../../../settings/Layout";
|
||||
|
|
|
@ -24,6 +24,7 @@ import AccessibleButton from "./AccessibleButton";
|
|||
import {_t} from "../../../languageHandler";
|
||||
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
||||
idp: IIdentityProvider;
|
||||
|
@ -72,7 +73,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
brandClass = `mx_SSOButton_brand_${brandName}`;
|
||||
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
|
||||
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
|
||||
const src = matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true);
|
||||
const src = mediaFromMxc(idp.icon).getSquareThumbnailHttp(24);
|
||||
icon = <img src={src} height="24" width="24" alt={idp.name} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
// A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
|
@ -130,11 +131,11 @@ export default class TagTile extends React.Component {
|
|||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.tag;
|
||||
const avatarHeight = 32;
|
||||
const avatarSize = 32;
|
||||
|
||||
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
) : null;
|
||||
const httpUrl = profile.avatarUrl
|
||||
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarSize)
|
||||
: null;
|
||||
|
||||
const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
|
||||
const className = classNames({
|
||||
|
@ -180,8 +181,8 @@ export default class TagTile extends React.Component {
|
|||
name={name}
|
||||
idName={this.props.tag}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
/>
|
||||
{contextButton}
|
||||
{badgeElement}
|
||||
|
|
|
@ -46,12 +46,14 @@ export default class TextWithTooltip extends React.Component {
|
|||
render() {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
|
||||
const {class: className, children, tooltip, tooltipClass, ...props} = this.props;
|
||||
|
||||
return (
|
||||
<span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}>
|
||||
{this.props.children}
|
||||
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
|
||||
{children}
|
||||
{this.state.hover && <Tooltip
|
||||
label={this.props.tooltip}
|
||||
tooltipClassName={this.props.tooltipClass}
|
||||
label={tooltip}
|
||||
tooltipClassName={tooltipClass}
|
||||
className={"mx_TextWithTooltip_tooltip"} /> }
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/Contex
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
// XXX this class copies a lot from RoomTile.js
|
||||
@replaceableComponent("views.groups.GroupInviteTile")
|
||||
|
@ -117,8 +118,9 @@ export default class GroupInviteTile extends React.Component {
|
|||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
const groupName = this.props.group.name || this.props.group.groupId;
|
||||
const httpAvatarUrl = this.props.group.avatarUrl ?
|
||||
this.context.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
|
||||
const httpAvatarUrl = this.props.group.avatarUrl
|
||||
? mediaFromMxc(this.props.group.avatarUrl).getSquareThumbnailHttp(24)
|
||||
: null;
|
||||
|
||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import { GroupMemberType } from '../../../groups';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.groups.GroupMemberTile")
|
||||
export default class GroupMemberTile extends React.Component {
|
||||
|
@ -46,10 +47,9 @@ export default class GroupMemberTile extends React.Component {
|
|||
const EntityTile = sdk.getComponent('rooms.EntityTile');
|
||||
|
||||
const name = this.props.member.displayname || this.props.member.userId;
|
||||
const avatarUrl = this.context.mxcUrlToHttp(
|
||||
this.props.member.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
const avatarUrl = this.props.member.avatarUrl
|
||||
? mediaFromMxc(this.props.member.avatarUrl).getSquareThumbnailHttp(36)
|
||||
: null;
|
||||
|
||||
const av = (
|
||||
<BaseAvatar
|
||||
|
|
|
@ -25,6 +25,7 @@ import GroupStore from '../../../stores/GroupStore';
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.groups.GroupRoomInfo")
|
||||
export default class GroupRoomInfo extends React.Component {
|
||||
|
@ -204,10 +205,8 @@ export default class GroupRoomInfo extends React.Component {
|
|||
const avatarUrl = this.state.groupRoom.avatarUrl;
|
||||
let avatarElement;
|
||||
if (avatarUrl) {
|
||||
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
avatarElement = (<div className="mx_MemberInfo_avatar">
|
||||
<img src={httpUrl} />
|
||||
</div>);
|
||||
const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800);
|
||||
avatarElement = <div className="mx_MemberInfo_avatar"><img src={httpUrl} /></div>;
|
||||
}
|
||||
|
||||
const groupRoomName = this.state.groupRoom.displayname;
|
||||
|
|
|
@ -21,6 +21,7 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import { GroupRoomType } from '../../../groups';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.groups.GroupRoomTile")
|
||||
class GroupRoomTile extends React.Component {
|
||||
|
@ -42,10 +43,9 @@ class GroupRoomTile extends React.Component {
|
|||
render() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const avatarUrl = this.context.mxcUrlToHttp(
|
||||
this.props.groupRoom.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
const avatarUrl = this.props.groupRoom.avatarUrl
|
||||
? mediaFromMxc(this.props.groupRoom.avatarUrl).getSquareThumbnailHttp(36)
|
||||
: null;
|
||||
|
||||
const av = (
|
||||
<BaseAvatar name={this.props.groupRoom.displayname}
|
||||
|
|
|
@ -22,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import FlairStore from '../../../stores/FlairStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
function nop() {}
|
||||
|
||||
|
@ -73,8 +74,9 @@ class GroupTile extends React.Component {
|
|||
const descElement = this.props.showDescription ?
|
||||
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
|
||||
<div />;
|
||||
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null;
|
||||
const httpUrl = profile.avatarUrl
|
||||
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
|
||||
: null;
|
||||
|
||||
let avatarElement = (
|
||||
<div className="mx_GroupTile_avatar">
|
||||
|
|
|
@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
|
|||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
||||
import {formatTime} from '../../../DateUtils';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
|
||||
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
|
@ -76,12 +76,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
_onViewSourceClick = () => {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, {
|
||||
roomId: this.props.mxEvent.getRoomId(),
|
||||
eventId: this.props.mxEvent.getId(),
|
||||
content: this.props.mxEvent.event,
|
||||
isEncrypted: this.props.mxEvent.isEncrypted(),
|
||||
// FIXME: _clearEvent is private
|
||||
decryptedContent: this.props.mxEvent._clearEvent,
|
||||
mxEvent: this.props.mxEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
};
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
import React from 'react';
|
||||
import MFileBody from './MFileBody';
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.messages.MAudioBody")
|
||||
export default class MAudioBody extends React.Component {
|
||||
|
@ -41,11 +41,11 @@ export default class MAudioBody extends React.Component {
|
|||
}
|
||||
|
||||
_getContentUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
return media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2015, 2016, 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.
|
||||
|
@ -18,52 +17,24 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import filesize from 'filesize';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {decryptFile} from '../../../utils/DecryptFile';
|
||||
import Tinter from '../../../Tinter';
|
||||
import request from 'browser-request';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
|
||||
let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on
|
||||
|
||||
// A cached tinted copy of require("../../../../res/img/download.svg")
|
||||
let tintedDownloadImageURL;
|
||||
// Track a list of mounted MFileBody instances so that we can update
|
||||
// the require("../../../../res/img/download.svg") when the tint changes.
|
||||
let nextMountId = 0;
|
||||
const mounts = {};
|
||||
|
||||
/**
|
||||
* Updates the tinted copy of require("../../../../res/img/download.svg") when the tint changes.
|
||||
*/
|
||||
function updateTintedDownloadImage() {
|
||||
// Download the svg as an XML document.
|
||||
// We could cache the XML response here, but since the tint rarely changes
|
||||
// it's probably not worth it.
|
||||
// Also note that we can't use fetch here because fetch doesn't support
|
||||
// file URLs, which the download image will be if we're running from
|
||||
// the filesystem (like in an Electron wrapper).
|
||||
request({uri: require("../../../../res/img/download.svg")}, (err, response, body) => {
|
||||
if (err) return;
|
||||
|
||||
const svg = new DOMParser().parseFromString(body, "image/svg+xml");
|
||||
// Apply the fixups to the XML.
|
||||
const fixups = Tinter.calcSvgFixups([{contentDocument: svg}]);
|
||||
Tinter.applySvgFixups(fixups);
|
||||
// Encoded the fixed up SVG as a data URL.
|
||||
const svgString = new XMLSerializer().serializeToString(svg);
|
||||
tintedDownloadImageURL = "data:image/svg+xml;base64," + window.btoa(svgString);
|
||||
// Notify each mounted MFileBody that the URL has changed.
|
||||
Object.keys(mounts).forEach(function(id) {
|
||||
mounts[id].tint();
|
||||
});
|
||||
});
|
||||
async function cacheDownloadIcon() {
|
||||
if (downloadIconUrl) return; // cached already
|
||||
const svg = await fetch(require("../../../../res/img/download.svg")).then(r => r.text());
|
||||
downloadIconUrl = "data:image/svg+xml;base64," + window.btoa(svg);
|
||||
}
|
||||
|
||||
Tinter.registerTintable(updateTintedDownloadImage);
|
||||
// Cache the asset immediately
|
||||
cacheDownloadIcon();
|
||||
|
||||
// User supplied content can contain scripts, we have to be careful that
|
||||
// we don't accidentally run those script within the same origin as the
|
||||
|
@ -106,6 +77,7 @@ function computedStyle(element) {
|
|||
}
|
||||
const style = window.getComputedStyle(element, null);
|
||||
let cssText = style.cssText;
|
||||
// noinspection EqualityComparisonWithCoercionJS
|
||||
if (cssText == "") {
|
||||
// Firefox doesn't implement ".cssText" for computed styles.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
|
||||
|
@ -145,7 +117,6 @@ export default class MFileBody extends React.Component {
|
|||
|
||||
this._iframe = createRef();
|
||||
this._dummyLink = createRef();
|
||||
this._downloadImage = createRef();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -178,16 +149,8 @@ export default class MFileBody extends React.Component {
|
|||
}
|
||||
|
||||
_getContentUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Add this to the list of mounted components to receive notifications
|
||||
// when the tint changes.
|
||||
this.id = nextMountId++;
|
||||
mounts[this.id] = this;
|
||||
this.tint();
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
return media.srcHttp;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -196,34 +159,12 @@ export default class MFileBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Remove this from the list of mounted components
|
||||
delete mounts[this.id];
|
||||
}
|
||||
|
||||
tint = () => {
|
||||
// Update our tinted copy of require("../../../../res/img/download.svg")
|
||||
if (this._downloadImage.current) {
|
||||
this._downloadImage.current.src = tintedDownloadImageURL;
|
||||
}
|
||||
if (this._iframe.current) {
|
||||
// If the attachment is encrypted then the download image
|
||||
// will be inside the iframe so we wont be able to update
|
||||
// it directly.
|
||||
this._iframe.current.contentWindow.postMessage({
|
||||
imgSrc: tintedDownloadImageURL,
|
||||
style: computedStyle(this._dummyLink.current),
|
||||
}, "*");
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const text = this.presentableTextForFile(content);
|
||||
const isEncrypted = content.file !== undefined;
|
||||
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
|
||||
const contentUrl = this._getContentUrl();
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const fileSize = content.info ? content.info.size : null;
|
||||
const fileType = content.info ? content.info.mimetype : "application/octet-stream";
|
||||
|
||||
|
@ -280,7 +221,8 @@ export default class MFileBody extends React.Component {
|
|||
// When the iframe loads we tell it to render a download link
|
||||
const onIframeLoad = (ev) => {
|
||||
ev.target.contentWindow.postMessage({
|
||||
imgSrc: tintedDownloadImageURL,
|
||||
imgSrc: downloadIconUrl,
|
||||
imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon.
|
||||
style: computedStyle(this._dummyLink.current),
|
||||
blob: this.state.decryptedBlob,
|
||||
// Set a download attribute for encrypted files so that the file
|
||||
|
@ -384,7 +326,7 @@ export default class MFileBody extends React.Component {
|
|||
{placeholder}
|
||||
<div className="mx_MFileBody_download">
|
||||
<a {...downloadProps}>
|
||||
<img src={tintedDownloadImageURL} width="12" height="14" ref={this._downloadImage} />
|
||||
<span className="mx_MFileBody_download_icon" />
|
||||
{ _t("Download %(text)s", { text: text }) }
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -28,6 +28,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.messages.MImageBody")
|
||||
export default class MImageBody extends React.Component {
|
||||
|
@ -70,7 +71,7 @@ export default class MImageBody extends React.Component {
|
|||
this._image = createRef();
|
||||
}
|
||||
|
||||
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
|
||||
// FIXME: factor this out and apply it to MVideoBody and MAudioBody too!
|
||||
onClientSync(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
|
@ -167,16 +168,16 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
_getContentUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return this.context.mxcUrlToHttp(content.url);
|
||||
return media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
_getThumbUrl() {
|
||||
// FIXME: the dharma skin lets images grow as wide as you like, rather than capped to 800x600.
|
||||
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
|
||||
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
|
||||
// thumbnail resolution will be unnecessarily reduced.
|
||||
// custom timeline widths seems preferable.
|
||||
|
@ -185,21 +186,19 @@ export default class MImageBody extends React.Component {
|
|||
const thumbHeight = Math.round(600 * pixelRatio);
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(content);
|
||||
|
||||
if (media.isEncrypted) {
|
||||
// Don't use the thumbnail for clients wishing to autoplay gifs.
|
||||
if (this.state.decryptedThumbnailUrl) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
}
|
||||
return this.state.decryptedUrl;
|
||||
} else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) {
|
||||
} else if (content.info && content.info.mimetype === "image/svg+xml" && media.hasThumbnail) {
|
||||
// special case to return clientside sender-generated thumbnails for SVGs, if any,
|
||||
// given we deliberately don't thumbnail them serverside to prevent
|
||||
// billion lol attacks and similar
|
||||
return this.context.mxcUrlToHttp(
|
||||
content.info.thumbnail_url,
|
||||
thumbWidth,
|
||||
thumbHeight,
|
||||
);
|
||||
return media.getThumbnailHttp(thumbWidth, thumbHeight, 'scale');
|
||||
} else {
|
||||
// we try to download the correct resolution
|
||||
// for hi-res images (like retina screenshots).
|
||||
|
@ -218,7 +217,7 @@ export default class MImageBody extends React.Component {
|
|||
pixelRatio === 1.0 ||
|
||||
(!info || !info.w || !info.h || !info.size)
|
||||
) {
|
||||
return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
|
||||
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
|
||||
} else {
|
||||
// we should only request thumbnails if the image is bigger than 800x600
|
||||
// (or 1600x1200 on retina) otherwise the image in the timeline will just
|
||||
|
@ -233,24 +232,17 @@ export default class MImageBody extends React.Component {
|
|||
info.w > thumbWidth ||
|
||||
info.h > thumbHeight
|
||||
);
|
||||
const isLargeFileSize = info.size > 1*1024*1024;
|
||||
const isLargeFileSize = info.size > 1*1024*1024; // 1mb
|
||||
|
||||
if (isLargeFileSize && isLargerThanThumbnail) {
|
||||
// image is too large physically and bytewise to clutter our timeline so
|
||||
// we ask for a thumbnail, despite knowing that it will be max 800x600
|
||||
// despite us being retina (as synapse doesn't do 1600x1200 thumbs yet).
|
||||
return this.context.mxcUrlToHttp(
|
||||
content.url,
|
||||
thumbWidth,
|
||||
thumbHeight,
|
||||
);
|
||||
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
|
||||
} else {
|
||||
// download the original image otherwise, so we can scale it client side
|
||||
// to take pixelRatio into account.
|
||||
// ( no width/height means we want the original image)
|
||||
return this.context.mxcUrlToHttp(
|
||||
content.url,
|
||||
);
|
||||
return media.srcHttp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import MFileBody from './MFileBody';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
|
@ -76,11 +76,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
private getContentUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
return media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,10 +91,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private getThumbUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
} else if (content.info && content.info.thumbnail_url) {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
|
||||
} else if (media.hasThumbnail) {
|
||||
return media.thumbnailHttp;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
|
||||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EventStatus } from 'matrix-js-sdk';
|
||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
|
|
|
@ -71,6 +71,10 @@ export default class MessageEvent extends React.Component {
|
|||
'm.file': sdk.getComponent('messages.MFileBody'),
|
||||
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
||||
'm.video': sdk.getComponent('messages.MVideoBody'),
|
||||
|
||||
// TODO: @@ TravisR: Use labs flag determination.
|
||||
// MSC: https://github.com/matrix-org/matrix-doc/pull/2516
|
||||
'org.matrix.msc2516.voice': sdk.getComponent('messages.MAudioBody'),
|
||||
};
|
||||
const evTypes = {
|
||||
'm.sticker': sdk.getComponent('messages.MStickerBody'),
|
||||
|
|
|
@ -24,6 +24,7 @@ import * as sdk from '../../../index';
|
|||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.messages.RoomAvatarEvent")
|
||||
export default class RoomAvatarEvent extends React.Component {
|
||||
|
@ -35,7 +36,7 @@ export default class RoomAvatarEvent extends React.Component {
|
|||
onAvatarClick = () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ev = this.props.mxEvent;
|
||||
const httpUrl = cli.mxcUrlToHttp(ev.getContent().url);
|
||||
const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp;
|
||||
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
|
||||
|
|
|
@ -216,12 +216,12 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
|
||||
_addLineNumbers(pre) {
|
||||
// Calculate number of lines in pre
|
||||
const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
|
||||
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
|
||||
const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0];
|
||||
// Calculate number of lines in pre
|
||||
const number = pre.innerHTML.split(/\n/).length;
|
||||
// Iterate through lines starting with 1 (number of the first line is 1)
|
||||
for (let i = 1; i < number; i++) {
|
||||
for (let i = 1; i <= number; i++) {
|
||||
lineNumbers.innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -1424,14 +1425,14 @@ const UserInfoHeader: React.FC<{
|
|||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
|
||||
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: member.name,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
}, [cli, member]);
|
||||
}, [member]);
|
||||
|
||||
const avatarElement = (
|
||||
<div className="mx_UserInfo_avatar">
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
|
|
|
@ -21,6 +21,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|||
import Field from "../elements/Field";
|
||||
import * as sdk from "../../../index";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
// TODO: Merge with ProfileSettings?
|
||||
@replaceableComponent("views.room_settings.RoomProfileSettings")
|
||||
|
@ -38,7 +39,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
|
||||
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
|
||||
let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
|
||||
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
|
||||
if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96);
|
||||
|
||||
const topicEvent = room.currentState.getStateEvents("m.room.topic", "");
|
||||
const topic = topicEvent && topicEvent.getContent() ? topicEvent.getContent()['topic'] : '';
|
||||
|
@ -112,7 +113,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
if (this.state.avatarFile) {
|
||||
const uri = await client.uploadContent(this.state.avatarFile);
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: uri}, '');
|
||||
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
|
||||
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
|
|
|
@ -93,6 +93,7 @@ interface IProps {
|
|||
placeholder?: string;
|
||||
label?: string;
|
||||
initialCaret?: DocumentOffset;
|
||||
disabled?: boolean;
|
||||
|
||||
onChange?();
|
||||
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
|
||||
|
@ -672,6 +673,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
});
|
||||
const classes = classNames("mx_BasicMessageComposer_input", {
|
||||
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
|
||||
|
||||
// TODO: @@ TravisR: This doesn't work properly. The composer resets in a strange way.
|
||||
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
|
||||
});
|
||||
|
||||
const shortcuts = {
|
||||
|
@ -704,6 +708,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
aria-expanded={Boolean(this.state.autoComplete)}
|
||||
aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined}
|
||||
dir="auto"
|
||||
aria-disabled={this.props.disabled}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import {parseEvent} from '../../../editor/deserialize';
|
|||
import {PartCreator} from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import classNames from 'classnames';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
|
|
@ -28,7 +28,7 @@ import * as sdk from "../../../index";
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout, LayoutPropType} from "../../../settings/Layout";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import {formatTime} from "../../../DateUtils";
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
||||
|
|
|
@ -26,6 +26,7 @@ import Modal from "../../../Modal";
|
|||
import * as ImageUtils from "../../../ImageUtils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
||||
export default class LinkPreviewWidget extends React.Component {
|
||||
|
@ -83,7 +84,7 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
|
||||
let src = p["og:image"];
|
||||
if (src && src.startsWith("mxc://")) {
|
||||
src = MatrixClientPeg.get().mxcUrlToHttp(src);
|
||||
src = mediaFromMxc(src).srcHttp;
|
||||
}
|
||||
|
||||
const params = {
|
||||
|
@ -109,9 +110,11 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
if (!SettingsStore.getValue("showImages")) {
|
||||
image = null; // Don't render a button to show the image, just hide it outright
|
||||
}
|
||||
const imageMaxWidth = 100; const imageMaxHeight = 100;
|
||||
const imageMaxWidth = 100;
|
||||
const imageMaxHeight = 100;
|
||||
if (image && image.startsWith("mxc://")) {
|
||||
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
|
||||
// We deliberately don't want a square here, so use the source HTTP thumbnail function
|
||||
image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, 'scale');
|
||||
}
|
||||
|
||||
let thumbHeight = imageMaxHeight;
|
||||
|
|
|
@ -33,6 +33,7 @@ import WidgetStore from "../../../stores/WidgetStore";
|
|||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
|
@ -187,6 +188,7 @@ export default class MessageComposer extends React.Component {
|
|||
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
|
||||
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
|
||||
isComposerEmpty: true,
|
||||
haveRecording: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -325,6 +327,10 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onVoiceUpdate = (haveRecording: boolean) => {
|
||||
this.setState({haveRecording});
|
||||
};
|
||||
|
||||
render() {
|
||||
const controls = [
|
||||
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||
|
@ -346,17 +352,32 @@ export default class MessageComposer extends React.Component {
|
|||
permalinkCreator={this.props.permalinkCreator}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
// TODO: @@ TravisR - Disabling the composer doesn't work
|
||||
disabled={this.state.haveRecording}
|
||||
/>,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
|
||||
if (!this.state.haveRecording) {
|
||||
controls.push(
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Widgets) &&
|
||||
SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
|
||||
SettingsStore.getValue("MessageComposerInput.showStickersButton") &&
|
||||
!this.state.haveRecording) {
|
||||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
if (!this.state.isComposerEmpty) {
|
||||
if (SettingsStore.getValue("feature_voice_messages")) {
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
room={this.props.room}
|
||||
onRecording={this.onVoiceUpdate} />);
|
||||
}
|
||||
|
||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||
controls.push(
|
||||
<SendButton key="controls_send" onClick={this.sendMessage} />,
|
||||
);
|
||||
|
|
|
@ -85,8 +85,8 @@ class FormatButton extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
as="span"
|
||||
role="button"
|
||||
element="button"
|
||||
type="button"
|
||||
onClick={this.props.onClick}
|
||||
title={this.props.label}
|
||||
tooltip={tooltip}
|
||||
|
|
|
@ -28,6 +28,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|||
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
const NewRoomIntro = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
@ -100,17 +101,48 @@ const NewRoomIntro = () => {
|
|||
});
|
||||
}
|
||||
|
||||
let buttons;
|
||||
if (room.canInvite(cli.getUserId())) {
|
||||
const onInviteClick = () => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
};
|
||||
let parentSpace;
|
||||
if (
|
||||
SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) &&
|
||||
SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
|
||||
) {
|
||||
parentSpace = SpaceStore.instance.activeSpace;
|
||||
}
|
||||
|
||||
let buttons;
|
||||
if (parentSpace) {
|
||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
|
||||
<AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}
|
||||
</AccessibleButton>
|
||||
{ room.canInvite(cli.getUserId()) && <AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary_outline"
|
||||
onClick={() => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{_t("Invite to just this room")}
|
||||
</AccessibleButton> }
|
||||
</div>;
|
||||
} else if (room.canInvite(cli.getUserId())) {
|
||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{_t("Invite to this room")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||
|
|
|
@ -18,10 +18,9 @@ import * as sdk from '../../../index';
|
|||
import React, {createRef} from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import PropTypes from 'prop-types';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
export function getDisplayAliasForRoom(room) {
|
||||
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
||||
|
@ -100,13 +99,14 @@ export default class RoomDetailRow extends React.Component {
|
|||
{ guestJoin }
|
||||
</div>) : <div />;
|
||||
|
||||
let avatarUrl = null;
|
||||
if (room.avatarUrl) avatarUrl = mediaFromMxc(room.avatarUrl).getSquareThumbnailHttp(24);
|
||||
|
||||
return <tr key={room.roomId} onClick={this.onClick} onMouseDown={this.props.onMouseDown}>
|
||||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={24} height={24} resizeMethod='crop'
|
||||
name={name} idName={name}
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
room.avatarUrl, 24, 24, "crop")} />
|
||||
url={avatarUrl} />
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomDescription">
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
|
|
|
@ -20,6 +20,7 @@ import React, { ReactComponentElement } from "react";
|
|||
import { Dispatcher } from "flux";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
|
@ -48,12 +49,15 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore";
|
||||
import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
|
||||
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
|
||||
import { showRoomInviteDialog } from "../../../RoomInvite";
|
||||
import Modal from "../../../Modal";
|
||||
import SpacePublicShare from "../spaces/SpacePublicShare";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -62,6 +66,7 @@ interface IProps {
|
|||
onResize: () => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
isMinimized: boolean;
|
||||
activeSpace: Room;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -194,8 +199,8 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
: _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore space rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomList_iconBrowse"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -424,6 +429,25 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
||||
};
|
||||
|
||||
private onSpaceInviteClick = () => {
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
if (this.props.activeSpace.getJoinRule() === "public") {
|
||||
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
|
||||
title: _t("Invite to %(spaceName)s", { spaceName: this.props.activeSpace.name }),
|
||||
description: <React.Fragment>
|
||||
<span>{ _t("Share your public space") }</span>
|
||||
<SpacePublicShare space={this.props.activeSpace} onFinished={() => modal.close()} />
|
||||
</React.Fragment>,
|
||||
fixedWidth: false,
|
||||
button: false,
|
||||
className: "mx_SpacePanel_sharePublicSpace",
|
||||
hasCloseButton: true,
|
||||
});
|
||||
} else {
|
||||
showRoomInviteDialog(this.props.activeSpace.roomId, initialText);
|
||||
}
|
||||
};
|
||||
|
||||
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
|
||||
return this.state.suggestedRooms.map(room => {
|
||||
const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
|
||||
|
@ -569,7 +593,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
kind="link"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{_t("Explore all public rooms")}
|
||||
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (this.props.activeSpace) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{ _t("Quick actions") }</div>
|
||||
{ this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_spaceInvite"
|
||||
onClick={this.onSpaceInviteClick}
|
||||
>
|
||||
{_t("Invite people")}
|
||||
</AccessibleButton> }
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_spaceExplore"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{_t("Explore rooms")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
||||
|
|
|
@ -333,6 +333,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.setState({generalMenuPosition: null}); // hide the menu
|
||||
};
|
||||
|
||||
private onInviteClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_invite',
|
||||
roomId: this.props.room.roomId,
|
||||
});
|
||||
this.setState({generalMenuPosition: null}); // hide the menu
|
||||
};
|
||||
|
||||
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -453,6 +464,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
|
||||
const lowPriorityLabel = _t("Low Priority");
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const canInvite = this.props.room.canInvite(userId);
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...contextMenuBelow(this.state.generalMenuPosition)}
|
||||
onFinished={this.onCloseGeneralMenu}
|
||||
|
@ -472,7 +485,13 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
label={lowPriorityLabel}
|
||||
iconClassName="mx_RoomTile_iconArrowDown"
|
||||
/>
|
||||
|
||||
{canInvite ? (
|
||||
<IconizedContextMenuOption
|
||||
onClick={this.onInviteClick}
|
||||
label={_t("Invite People")}
|
||||
iconClassName="mx_RoomTile_iconInvite"
|
||||
/>
|
||||
) : null}
|
||||
<IconizedContextMenuOption
|
||||
onClick={this.onOpenRoomSettings}
|
||||
label={_t("Settings")}
|
||||
|
|
|
@ -33,7 +33,7 @@ import ReplyThread from "../elements/ReplyThread";
|
|||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import SendHistoryManager from "../../../SendHistoryManager";
|
||||
import {getCommand} from '../../../SlashCommands';
|
||||
import {CommandCategories, getCommand} from '../../../SlashCommands';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
|
@ -120,6 +120,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
permalinkCreator: PropTypes.object.isRequired,
|
||||
replyToEvent: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -290,15 +291,22 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
return [getCommand(this.props.room.roomId, commandText), commandText];
|
||||
const {cmd, args} = getCommand(commandText);
|
||||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
async _runSlashCommand(fn) {
|
||||
const cmd = fn();
|
||||
let error = cmd.error;
|
||||
if (cmd.promise) {
|
||||
async _runSlashCommand(cmd, args) {
|
||||
const result = cmd.run(this.props.room.roomId, args);
|
||||
let messageContent;
|
||||
let error = result.error;
|
||||
if (result.promise) {
|
||||
try {
|
||||
await cmd.promise;
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
// The command returns a modified message that we need to pass on
|
||||
messageContent = await result.promise;
|
||||
} else {
|
||||
await result.promise;
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
@ -307,7 +315,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
console.error("Command failure: %s", error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!cmd.promise;
|
||||
const isServerError = !!result.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
||||
let errText;
|
||||
|
@ -325,6 +333,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
});
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
if (messageContent) return messageContent;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,13 +342,22 @@ export default class SendMessageComposer extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const replyToEvent = this.props.replyToEvent;
|
||||
let shouldSend = true;
|
||||
let content;
|
||||
|
||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||
const [cmd, commandText] = this._getSlashCommand();
|
||||
const [cmd, args, commandText] = this._getSlashCommand();
|
||||
if (cmd) {
|
||||
shouldSend = false;
|
||||
this._runSlashCommand(cmd);
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
content = await this._runSlashCommand(cmd, args);
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator);
|
||||
}
|
||||
} else {
|
||||
this._runSlashCommand(cmd, args);
|
||||
shouldSend = false;
|
||||
}
|
||||
} else {
|
||||
// ask the user if their unknown command should be sent as a message
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
@ -374,11 +392,12 @@ export default class SendMessageComposer extends React.Component {
|
|||
this._sendQuickReaction();
|
||||
}
|
||||
|
||||
const replyToEvent = this.props.replyToEvent;
|
||||
if (shouldSend) {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const {roomId} = this.props.room;
|
||||
const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
|
||||
if (!content) {
|
||||
content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
|
||||
}
|
||||
// don't bother sending an empty message
|
||||
if (!content.body.trim()) return;
|
||||
|
||||
|
@ -453,12 +472,17 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// should save state when editor has contents or reply is open
|
||||
_shouldSaveStoredEditorState = () => {
|
||||
return !this.model.isEmpty || this.props.replyToEvent;
|
||||
}
|
||||
|
||||
_saveStoredEditorState = () => {
|
||||
if (this.model.isEmpty) {
|
||||
this._clearStoredEditorState();
|
||||
} else {
|
||||
if (this._shouldSaveStoredEditorState()) {
|
||||
const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent);
|
||||
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
|
||||
} else {
|
||||
this._clearStoredEditorState();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -556,6 +580,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
label={this.props.placeholder}
|
||||
placeholder={this.props.placeholder}
|
||||
onPaste={this._onPaste}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import * as sdk from "../../../index";
|
||||
|
|
88
src/components/views/rooms/VoiceRecordComposerTile.tsx
Normal file
88
src/components/views/rooms/VoiceRecordComposerTile.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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 AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import React from "react";
|
||||
import {VoiceRecorder} from "../../../voice/VoiceRecorder";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
onRecording: (haveRecording: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
recorder?: VoiceRecorder;
|
||||
}
|
||||
|
||||
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
recorder: null, // not recording by default
|
||||
};
|
||||
}
|
||||
|
||||
private onStartStopVoiceMessage = async () => {
|
||||
// TODO: @@ TravisR: We do not want to auto-send on stop.
|
||||
if (this.state.recorder) {
|
||||
await this.state.recorder.stop();
|
||||
const mxc = await this.state.recorder.upload();
|
||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||
body: "Voice message",
|
||||
msgtype: "org.matrix.msc2516.voice",
|
||||
url: mxc,
|
||||
});
|
||||
this.setState({recorder: null});
|
||||
this.props.onRecording(false);
|
||||
return;
|
||||
}
|
||||
const recorder = new VoiceRecorder(MatrixClientPeg.get());
|
||||
await recorder.start();
|
||||
this.props.onRecording(true);
|
||||
// TODO: @@ TravisR: Run through EQ component
|
||||
// recorder.frequencyData.onUpdate((freq) => {
|
||||
// console.log('@@ UPDATE', freq);
|
||||
// });
|
||||
this.setState({recorder});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const classes = classNames({
|
||||
'mx_MessageComposer_button': !this.state.recorder,
|
||||
'mx_MessageComposer_voiceMessage': !this.state.recorder,
|
||||
'mx_VoiceRecordComposerTile_stop': !!this.state.recorder,
|
||||
});
|
||||
|
||||
let tooltip = _t("Record a voice message");
|
||||
if (!!this.state.recorder) {
|
||||
// TODO: @@ TravisR: Change to match behaviour
|
||||
tooltip = _t("Stop & send recording");
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className={classes}
|
||||
onClick={this.onStartStopVoiceMessage}
|
||||
title={tooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,9 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Pill from "../elements/Pill";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
|
@ -27,6 +25,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
interface IProps {
|
||||
ev: MatrixEvent;
|
||||
|
@ -114,10 +113,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
|
|||
let networkIcon;
|
||||
|
||||
if (protocol.avatar_url) {
|
||||
const avatarUrl = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
protocol.avatar_url, 64, 64, "crop",
|
||||
);
|
||||
const avatarUrl = mediaFromMxc(protocol.avatar_url).getSquareThumbnailHttp(64);
|
||||
|
||||
networkIcon = <BaseAvatar className="protocol-icon"
|
||||
width={48}
|
||||
|
|
|
@ -21,6 +21,7 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.settings.ChangeAvatar")
|
||||
export default class ChangeAvatar extends React.Component {
|
||||
|
@ -117,7 +118,7 @@ export default class ChangeAvatar extends React.Component {
|
|||
httpPromise.then(function() {
|
||||
self.setState({
|
||||
phase: ChangeAvatar.Phases.Display,
|
||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),
|
||||
avatarUrl: mediaFromMxc(newUrl).srcHttp,
|
||||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
|
|
|
@ -190,7 +190,7 @@ export default class EventIndexPanel extends React.Component {
|
|||
}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
} else if (!EventIndexPeg.platformHasSupport()) {
|
||||
eventIndexingSettings = (
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{
|
||||
|
@ -208,6 +208,23 @@ export default class EventIndexPanel extends React.Component {
|
|||
}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
eventIndexingSettings = (
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<p>
|
||||
{_t("Message search initilisation failed")}
|
||||
</p>
|
||||
{EventIndexPeg.error && (
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<code>
|
||||
{EventIndexPeg.error.message}
|
||||
</code>
|
||||
</details>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return eventIndexingSettings;
|
||||
|
|
|
@ -24,6 +24,7 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
|||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.settings.ProfileSettings")
|
||||
export default class ProfileSettings extends React.Component {
|
||||
|
@ -32,7 +33,7 @@ export default class ProfileSettings extends React.Component {
|
|||
|
||||
const client = MatrixClientPeg.get();
|
||||
let avatarUrl = OwnProfileStore.instance.avatarMxc;
|
||||
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
|
||||
if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96);
|
||||
this.state = {
|
||||
userId: client.getUserId(),
|
||||
originalDisplayName: OwnProfileStore.instance.displayName,
|
||||
|
@ -97,7 +98,7 @@ export default class ProfileSettings extends React.Component {
|
|||
` (${this.state.avatarFile.size}) bytes`);
|
||||
const uri = await client.uploadContent(this.state.avatarFile);
|
||||
await client.setAvatarUrl(uri);
|
||||
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
|
||||
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
|
|
|
@ -203,6 +203,7 @@ export class EmailAddress extends React.Component {
|
|||
className="mx_ExistingEmailAddress_confirmBtn"
|
||||
kind="primary_sm"
|
||||
onClick={this.onContinueClick}
|
||||
disabled={this.state.continueDisabled}
|
||||
>
|
||||
{_t("Complete")}
|
||||
</AccessibleButton>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue