Merge remote-tracking branch 'origin/develop' into dbkr/line_1_2
This commit is contained in:
commit
5282c6bbe8
32 changed files with 789 additions and 77 deletions
|
@ -69,11 +69,15 @@ import AuxPanel from "../views/rooms/AuxPanel";
|
|||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import {XOR} from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import {containsEmoji} from '../../effects/utils';
|
||||
import {CHAT_EFFECTS} from '../../effects';
|
||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -248,6 +252,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
this.context.on("Event.decrypted", this.onEventDecrypted);
|
||||
this.context.on("event", this.onEvent);
|
||||
// Start listening for RoomViewStore updates
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
||||
|
@ -581,6 +587,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
this.context.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
this.context.removeListener("event", this.onEvent);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
|
@ -781,6 +789,30 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onEventDecrypted = (ev) => {
|
||||
if (ev.isDecryptionFailure()) return;
|
||||
this.handleEffects(ev);
|
||||
};
|
||||
|
||||
private onEvent = (ev) => {
|
||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
||||
this.handleEffects(ev);
|
||||
};
|
||||
|
||||
private handleEffects = (ev) => {
|
||||
if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all
|
||||
if (ev.getRoomId() !== this.state.room.roomId) return; // not for us
|
||||
|
||||
const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
|
||||
if (!notifState.isUnread) return;
|
||||
|
||||
CHAT_EFFECTS.forEach(effect => {
|
||||
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
|
||||
dis.dispatch({action: `effects.${effect.command}`});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private onRoomName = (room: Room) => {
|
||||
if (this.state.room && room.roomId == this.state.room.roomId) {
|
||||
this.forceUpdate();
|
||||
|
@ -1946,9 +1978,14 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
mx_RoomView_inCall: Boolean(activeCall),
|
||||
});
|
||||
|
||||
const showChatEffects = SettingsStore.getValue('showChatEffects');
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||
{showChatEffects && this.roomView.current &&
|
||||
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||
}
|
||||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={this.state.room}
|
||||
|
|
|
@ -149,7 +149,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
onRedactClick = () => {
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||
onFinished: async (proceed) => {
|
||||
onFinished: async (proceed, reason) => {
|
||||
if (!proceed) return;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -157,6 +157,8 @@ export default class MessageContextMenu extends React.Component {
|
|||
await cli.redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.props.mxEvent.getId(),
|
||||
undefined,
|
||||
reason ? { reason } : {},
|
||||
);
|
||||
} catch (e) {
|
||||
const code = e.errcode || e.statusCode;
|
||||
|
|
|
@ -23,15 +23,17 @@ import { _t } from '../../../languageHandler';
|
|||
*/
|
||||
export default class ConfirmRedactDialog extends React.Component {
|
||||
render() {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
|
||||
return (
|
||||
<QuestionDialog onFinished={this.props.onFinished}
|
||||
<TextInputDialog onFinished={this.props.onFinished}
|
||||
title={_t("Confirm Removal")}
|
||||
description={
|
||||
_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
placeholder={_t("Reason (optional)")}
|
||||
focus
|
||||
button={_t("Remove")}>
|
||||
</QuestionDialog>
|
||||
</TextInputDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import React, {createRef} from "react";
|
||||
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
|
||||
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
@ -47,9 +48,20 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
|
||||
const config = SdkConfig.get();
|
||||
this.defaultServer = config["validated_server_config"] as ValidatedServerConfig;
|
||||
const { serverConfig } = this.props;
|
||||
|
||||
let otherHomeserver = "";
|
||||
if (!serverConfig.isDefault) {
|
||||
if (serverConfig.isNameResolvable && serverConfig.hsName) {
|
||||
otherHomeserver = serverConfig.hsName;
|
||||
} else {
|
||||
otherHomeserver = serverConfig.hsUrl;
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
defaultChosen: this.props.serverConfig.isDefault,
|
||||
otherHomeserver: this.props.serverConfig.isDefault ? "" : this.props.serverConfig.hsUrl,
|
||||
defaultChosen: serverConfig.isDefault,
|
||||
otherHomeserver,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -69,10 +81,25 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
private validate = withValidation<this, { error?: string }>({
|
||||
deriveData: async ({ value: hsUrl }) => {
|
||||
// Always try and use the defaults first
|
||||
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
if (defaultConfig.hsUrl === hsUrl) return {};
|
||||
deriveData: async ({ value }) => {
|
||||
let hsUrl = value.trim(); // trim to account for random whitespace
|
||||
|
||||
// if the URL has no protocol, try validate it as a serverName via well-known
|
||||
if (!hsUrl.includes("://")) {
|
||||
try {
|
||||
const discoveryResult = await AutoDiscovery.findClientConfig(hsUrl);
|
||||
this.validatedConf = AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(hsUrl, discoveryResult);
|
||||
return {}; // we have a validated config, we don't need to try the other paths
|
||||
} catch (e) {
|
||||
console.error(`Attempted ${hsUrl} as a server_name but it failed`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// if we got to this stage then either the well-known failed or the URL had a protocol specified,
|
||||
// so validate statically only. If the URL has no protocol, default to https.
|
||||
if (!hsUrl.includes("://")) {
|
||||
hsUrl = "https://" + hsUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl);
|
||||
|
@ -81,17 +108,22 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
console.error(e);
|
||||
|
||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (!stateForError.isFatalError) {
|
||||
// carry on anyway
|
||||
this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, null, true);
|
||||
return {};
|
||||
} else {
|
||||
let error = _t("Unable to validate homeserver/identity server");
|
||||
if (stateForError.isFatalError) {
|
||||
let error = _t("Unable to validate homeserver");
|
||||
if (e.translatedMessage) {
|
||||
error = e.translatedMessage;
|
||||
}
|
||||
return { error };
|
||||
}
|
||||
|
||||
// try to carry on anyway
|
||||
try {
|
||||
this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, null, true);
|
||||
return {};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { error: _t("Invalid URL") };
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: [
|
||||
|
@ -153,7 +185,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
>
|
||||
<form className="mx_Dialog_content" id="mx_ServerPickerDialog" onSubmit={this.onSubmit}>
|
||||
<p>
|
||||
{_t("We call the places you where you can host your account ‘homeservers’.")} {text}
|
||||
{_t("We call the places where you can host your account ‘homeservers’.")} {text}
|
||||
</p>
|
||||
|
||||
<StyledRadioButton
|
||||
|
|
|
@ -146,7 +146,7 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
|||
const events = this.props.target.getLiveTimeline().getEvents();
|
||||
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = this.state.permalinkCreator.forRoom();
|
||||
matrixToUrl = this.state.permalinkCreator.forShareableRoom();
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
|
|
94
src/components/views/elements/EffectsOverlay.tsx
Normal file
94
src/components/views/elements/EffectsOverlay.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Copyright 2020 Nurjin Jafar
|
||||
Copyright 2020 Nordeck IT + Consulting GmbH.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ICanvasEffect from '../../../effects/ICanvasEffect';
|
||||
import {CHAT_EFFECTS} from '../../../effects'
|
||||
|
||||
interface IProps {
|
||||
roomWidth: number;
|
||||
}
|
||||
|
||||
const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const effectsRef = useRef<Map<string, ICanvasEffect>>(new Map<string, ICanvasEffect>());
|
||||
|
||||
const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
|
||||
if (!name) return null;
|
||||
let effect: ICanvasEffect | null = effectsRef.current[name] || null;
|
||||
if (effect === null) {
|
||||
const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
|
||||
try {
|
||||
const { default: Effect } = await import(`../../../effects/${name}`);
|
||||
effect = new Effect(options);
|
||||
effectsRef.current[name] = effect;
|
||||
} catch (err) {
|
||||
console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err);
|
||||
}
|
||||
}
|
||||
return effect;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const resize = () => {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.height = window.innerHeight;
|
||||
}
|
||||
};
|
||||
const onAction = (payload: { action: string }) => {
|
||||
const actionPrefix = 'effects.';
|
||||
if (payload.action.indexOf(actionPrefix) === 0) {
|
||||
const effect = payload.action.substr(actionPrefix.length);
|
||||
lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
|
||||
}
|
||||
}
|
||||
const dispatcherRef = dis.register(onAction);
|
||||
const canvas = canvasRef.current;
|
||||
canvas.height = window.innerHeight;
|
||||
window.addEventListener('resize', resize, true);
|
||||
|
||||
return () => {
|
||||
dis.unregister(dispatcherRef);
|
||||
window.removeEventListener('resize', resize);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
|
||||
for (const effect in currentEffects) {
|
||||
const effectModule: ICanvasEffect = currentEffects[effect];
|
||||
if (effectModule && effectModule.isRunning) {
|
||||
effectModule.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={roomWidth}
|
||||
style={{
|
||||
display: 'block',
|
||||
zIndex: 999999,
|
||||
pointerEvents: 'none',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default EffectsOverlay;
|
|
@ -67,7 +67,7 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let serverName = serverConfig.hsName;
|
||||
let serverName = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl;
|
||||
if (serverConfig.hsNameIsDifferent) {
|
||||
serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
|
||||
{serverConfig.hsName}
|
||||
|
|
|
@ -43,6 +43,7 @@ import RoomContext from "../../../contexts/RoomContext";
|
|||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -210,14 +211,6 @@ const onRoomSettingsClick = () => {
|
|||
defaultDispatcher.dispatch({ action: "open_room_settings" });
|
||||
};
|
||||
|
||||
const useMemberCount = (room: Room) => {
|
||||
const [count, setCount] = useState(room.getJoinedMembers().length);
|
||||
useEventEmitter(room.currentState, "RoomState.members", () => {
|
||||
setCount(room.getJoinedMembers().length);
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
|
@ -251,7 +244,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
const memberCount = useMemberCount(room);
|
||||
const memberCount = useRoomMemberCount(room);
|
||||
|
||||
return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
|
||||
|
|
|
@ -60,8 +60,9 @@ const NewRoomIntro = () => {
|
|||
{ caption && <p>{ caption }</p> }
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
const inRoom = room && room.getMyMembership() === "join";
|
||||
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
|
||||
const canAddTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
|
||||
const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
|
||||
|
||||
const onTopicClick = () => {
|
||||
dis.dispatch({
|
||||
|
@ -99,9 +100,25 @@ const NewRoomIntro = () => {
|
|||
});
|
||||
}
|
||||
|
||||
const onInviteClick = () => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
};
|
||||
let canInvite = inRoom;
|
||||
const powerLevels = room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
||||
const me = room.getMember(cli.getUserId());
|
||||
if (powerLevels && me && powerLevels.invite > me.powerLevel) {
|
||||
canInvite = false;
|
||||
}
|
||||
|
||||
let buttons;
|
||||
if (canInvite) {
|
||||
const onInviteClick = () => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
};
|
||||
|
||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
|
||||
{_t("Invite to this room")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||
body = <React.Fragment>
|
||||
|
@ -119,11 +136,7 @@ const NewRoomIntro = () => {
|
|||
roomName: () => <b>{ room.name }</b>,
|
||||
})}</p>
|
||||
<p>{topicText}</p>
|
||||
<div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
|
||||
{_t("Invite to this room")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{ buttons }
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,8 @@ import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {containsEmoji} from "../../../effects/utils";
|
||||
import {CHAT_EFFECTS} from '../../../effects';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
|
@ -326,6 +328,11 @@ export default class SendMessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CHAT_EFFECTS.forEach((effect) => {
|
||||
if (containsEmoji(content, effect.emojis)) {
|
||||
dis.dispatch({action: `effects.${effect.command}`});
|
||||
}
|
||||
});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'showAvatarChanges',
|
||||
'showDisplaynameChanges',
|
||||
'showImages',
|
||||
'showChatEffects',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
];
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue