Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17244
Conflicts: src/components/views/spaces/SpaceCreateMenu.tsx
This commit is contained in:
commit
83c30b2280
75 changed files with 1940 additions and 806 deletions
|
@ -542,6 +542,7 @@ export default class CallHandler extends EventEmitter {
|
|||
if (newMappedRoomId !== mappedRoomId) {
|
||||
this.removeCallForRoom(mappedRoomId);
|
||||
mappedRoomId = newMappedRoomId;
|
||||
console.log("Moving call to room " + mappedRoomId);
|
||||
this.calls.set(mappedRoomId, call);
|
||||
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
||||
}
|
||||
|
@ -607,6 +608,7 @@ export default class CallHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
private removeCallForRoom(roomId: string) {
|
||||
console.log("Removing call for room ", roomId);
|
||||
this.calls.delete(roomId);
|
||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||
}
|
||||
|
@ -680,6 +682,7 @@ export default class CallHandler extends EventEmitter {
|
|||
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
||||
const call = MatrixClientPeg.get().createCall(mappedRoomId);
|
||||
|
||||
console.log("Adding call for room ", roomId);
|
||||
this.calls.set(roomId, call);
|
||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||
if (transferee) {
|
||||
|
@ -812,6 +815,7 @@ export default class CallHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||
console.log("Adding call for room ", mappedRoomId);
|
||||
this.calls.set(mappedRoomId, call)
|
||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||
this.setCallListeners(call);
|
||||
|
|
|
@ -28,8 +28,6 @@ import encrypt from "browser-encrypt-attachment";
|
|||
import extractPngChunks from "png-chunks-extract";
|
||||
import Spinner from "./components/views/elements/Spinner";
|
||||
|
||||
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
||||
import "blueimp-canvas-to-blob";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import {
|
||||
|
|
|
@ -24,13 +24,6 @@ import {sleep} from "./utils/promise";
|
|||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
|
||||
// polyfill textencoder if necessary
|
||||
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
||||
let TextEncoder = window.TextEncoder;
|
||||
if (!TextEncoder) {
|
||||
TextEncoder = TextEncodingUtf8.TextEncoder;
|
||||
}
|
||||
|
||||
const INACTIVITY_TIME = 20; // seconds
|
||||
const HEARTBEAT_INTERVAL = 5_000; // ms
|
||||
const SESSION_UPDATE_INTERVAL = 60; // seconds
|
||||
|
|
|
@ -21,153 +21,161 @@ import SettingsStore from "./settings/SettingsStore";
|
|||
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
|
||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
|
||||
|
||||
function textForMemberEvent(ev) {
|
||||
// These functions are frequently used just to check whether an event has
|
||||
// any text to display at all. For this reason they return deferred values
|
||||
// to avoid the expense of looking up translations when they're not needed.
|
||||
|
||||
function textForMemberEvent(ev): () => string | null {
|
||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||
const prevContent = ev.getPrevContent();
|
||||
const content = ev.getContent();
|
||||
|
||||
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
||||
const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
||||
switch (content.membership) {
|
||||
case 'invite': {
|
||||
const threePidContent = content.third_party_invite;
|
||||
if (threePidContent) {
|
||||
if (threePidContent.display_name) {
|
||||
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
||||
return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
||||
targetName,
|
||||
displayName: threePidContent.display_name,
|
||||
});
|
||||
} else {
|
||||
return _t('%(targetName)s accepted an invitation.', {targetName});
|
||||
return () => _t('%(targetName)s accepted an invitation.', {targetName});
|
||||
}
|
||||
} else {
|
||||
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
||||
return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
||||
}
|
||||
}
|
||||
case 'ban':
|
||||
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||
return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
||||
case 'join':
|
||||
if (prevContent && prevContent.membership === 'join') {
|
||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||
return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
||||
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
||||
oldDisplayName: prevContent.displayname,
|
||||
displayName: content.displayname,
|
||||
});
|
||||
} else if (!prevContent.displayname && content.displayname) {
|
||||
return _t('%(senderName)s set their display name to %(displayName)s.', {
|
||||
return () => _t('%(senderName)s set their display name to %(displayName)s.', {
|
||||
senderName: ev.getSender(),
|
||||
displayName: content.displayname,
|
||||
});
|
||||
} else if (prevContent.displayname && !content.displayname) {
|
||||
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
||||
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
||||
senderName,
|
||||
oldDisplayName: prevContent.displayname,
|
||||
});
|
||||
} else if (prevContent.avatar_url && !content.avatar_url) {
|
||||
return _t('%(senderName)s removed their profile picture.', {senderName});
|
||||
return () => _t('%(senderName)s removed their profile picture.', {senderName});
|
||||
} else if (prevContent.avatar_url && content.avatar_url &&
|
||||
prevContent.avatar_url !== content.avatar_url) {
|
||||
return _t('%(senderName)s changed their profile picture.', {senderName});
|
||||
return () => _t('%(senderName)s changed their profile picture.', {senderName});
|
||||
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||
return _t('%(senderName)s set a profile picture.', {senderName});
|
||||
return () => _t('%(senderName)s set a profile picture.', {senderName});
|
||||
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||
// This is a null rejoin, it will only be visible if the Labs option is enabled
|
||||
return _t("%(senderName)s made no change.", {senderName});
|
||||
return () => _t("%(senderName)s made no change.", {senderName});
|
||||
} else {
|
||||
return "";
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||
return _t('%(targetName)s joined the room.', {targetName});
|
||||
return () => _t('%(targetName)s joined the room.', {targetName});
|
||||
}
|
||||
case 'leave':
|
||||
if (ev.getSender() === ev.getStateKey()) {
|
||||
if (prevContent.membership === "invite") {
|
||||
return _t('%(targetName)s rejected the invitation.', {targetName});
|
||||
return () => _t('%(targetName)s rejected the invitation.', {targetName});
|
||||
} else {
|
||||
return _t('%(targetName)s left the room.', {targetName});
|
||||
return () => _t('%(targetName)s left the room.', {targetName});
|
||||
}
|
||||
} else if (prevContent.membership === "ban") {
|
||||
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
||||
return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
||||
} else if (prevContent.membership === "invite") {
|
||||
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
||||
return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
||||
senderName,
|
||||
targetName,
|
||||
}) + ' ' + reason;
|
||||
}) + ' ' + getReason();
|
||||
} else if (prevContent.membership === "join") {
|
||||
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||
return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
||||
} else {
|
||||
return "";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function textForTopicEvent(ev) {
|
||||
function textForTopicEvent(ev): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
|
||||
return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
|
||||
senderDisplayName,
|
||||
topic: ev.getContent().topic,
|
||||
});
|
||||
}
|
||||
|
||||
function textForRoomNameEvent(ev) {
|
||||
function textForRoomNameEvent(ev): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
|
||||
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
||||
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
||||
return () => _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
||||
}
|
||||
if (ev.getPrevContent().name) {
|
||||
return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
||||
return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
||||
senderDisplayName,
|
||||
oldRoomName: ev.getPrevContent().name,
|
||||
newRoomName: ev.getContent().name,
|
||||
});
|
||||
}
|
||||
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
||||
return () => _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
||||
senderDisplayName,
|
||||
roomName: ev.getContent().name,
|
||||
});
|
||||
}
|
||||
|
||||
function textForTombstoneEvent(ev) {
|
||||
function textForTombstoneEvent(ev): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
|
||||
return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
|
||||
}
|
||||
|
||||
function textForJoinRulesEvent(ev) {
|
||||
function textForJoinRulesEvent(ev): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
switch (ev.getContent().join_rule) {
|
||||
case "public":
|
||||
return _t('%(senderDisplayName)s made the room public to whoever knows the link.', {senderDisplayName});
|
||||
return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
|
||||
senderDisplayName,
|
||||
});
|
||||
case "invite":
|
||||
return _t('%(senderDisplayName)s made the room invite only.', {senderDisplayName});
|
||||
return () => _t('%(senderDisplayName)s made the room invite only.', {
|
||||
senderDisplayName,
|
||||
});
|
||||
default:
|
||||
// The spec supports "knock" and "private", however nothing implements these.
|
||||
return _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
|
||||
return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
|
||||
senderDisplayName,
|
||||
rule: ev.getContent().join_rule,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function textForGuestAccessEvent(ev) {
|
||||
function textForGuestAccessEvent(ev): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
switch (ev.getContent().guest_access) {
|
||||
case "can_join":
|
||||
return _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
|
||||
return () => _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
|
||||
case "forbidden":
|
||||
return _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
|
||||
return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
|
||||
default:
|
||||
// There's no other options we can expect, however just for safety's sake we'll do this.
|
||||
return _t('%(senderDisplayName)s changed guest access to %(rule)s', {
|
||||
return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
|
||||
senderDisplayName,
|
||||
rule: ev.getContent().guest_access,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function textForRelatedGroupsEvent(ev) {
|
||||
function textForRelatedGroupsEvent(ev): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const groups = ev.getContent().groups || [];
|
||||
const prevGroups = ev.getPrevContent().groups || [];
|
||||
|
@ -175,17 +183,17 @@ function textForRelatedGroupsEvent(ev) {
|
|||
const removed = prevGroups.filter((g) => !groups.includes(g));
|
||||
|
||||
if (added.length && !removed.length) {
|
||||
return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
|
||||
return () => _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
|
||||
senderDisplayName,
|
||||
groups: added.join(', '),
|
||||
});
|
||||
} else if (!added.length && removed.length) {
|
||||
return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
|
||||
return () => _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
|
||||
senderDisplayName,
|
||||
groups: removed.join(', '),
|
||||
});
|
||||
} else if (added.length && removed.length) {
|
||||
return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
|
||||
return () => _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
|
||||
'%(oldGroups)s in this room.', {
|
||||
senderDisplayName,
|
||||
newGroups: added.join(', '),
|
||||
|
@ -193,11 +201,11 @@ function textForRelatedGroupsEvent(ev) {
|
|||
});
|
||||
} else {
|
||||
// Don't bother rendering this change (because there were no changes)
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function textForServerACLEvent(ev) {
|
||||
function textForServerACLEvent(ev): () => string | null {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const prevContent = ev.getPrevContent();
|
||||
const current = ev.getContent();
|
||||
|
@ -207,11 +215,11 @@ function textForServerACLEvent(ev) {
|
|||
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
||||
};
|
||||
|
||||
let text = "";
|
||||
let getText = null;
|
||||
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
||||
text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
|
||||
getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
|
||||
} else {
|
||||
text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
|
||||
getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
|
||||
}
|
||||
|
||||
if (!Array.isArray(current.allow)) {
|
||||
|
@ -220,24 +228,27 @@ function textForServerACLEvent(ev) {
|
|||
|
||||
// If we know for sure everyone is banned, mark the room as obliterated
|
||||
if (current.allow.length === 0) {
|
||||
return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used.");
|
||||
return () => getText() + " " +
|
||||
_t("🎉 All servers are banned from participating! This room can no longer be used.");
|
||||
}
|
||||
|
||||
return text;
|
||||
return getText;
|
||||
}
|
||||
|
||||
function textForMessageEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||
}
|
||||
return message;
|
||||
function textForMessageEvent(ev): () => string | null {
|
||||
return () => {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||
}
|
||||
return message;
|
||||
};
|
||||
}
|
||||
|
||||
function textForCanonicalAliasEvent(ev) {
|
||||
function textForCanonicalAliasEvent(ev): () => string | null {
|
||||
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const oldAlias = ev.getPrevContent().alias;
|
||||
const oldAltAliases = ev.getPrevContent().alt_aliases || [];
|
||||
|
@ -248,96 +259,100 @@ function textForCanonicalAliasEvent(ev) {
|
|||
|
||||
if (!removedAltAliases.length && !addedAltAliases.length) {
|
||||
if (newAlias) {
|
||||
return _t('%(senderName)s set the main address for this room to %(address)s.', {
|
||||
return () => _t('%(senderName)s set the main address for this room to %(address)s.', {
|
||||
senderName: senderName,
|
||||
address: ev.getContent().alias,
|
||||
});
|
||||
} else if (oldAlias) {
|
||||
return _t('%(senderName)s removed the main address for this room.', {
|
||||
return () => _t('%(senderName)s removed the main address for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
} else if (newAlias === oldAlias) {
|
||||
if (addedAltAliases.length && !removedAltAliases.length) {
|
||||
return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
|
||||
return () => _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
|
||||
senderName: senderName,
|
||||
addresses: addedAltAliases.join(", "),
|
||||
count: addedAltAliases.length,
|
||||
});
|
||||
} if (removedAltAliases.length && !addedAltAliases.length) {
|
||||
return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
|
||||
return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
|
||||
senderName: senderName,
|
||||
addresses: removedAltAliases.join(", "),
|
||||
count: removedAltAliases.length,
|
||||
});
|
||||
} if (removedAltAliases.length && addedAltAliases.length) {
|
||||
return _t('%(senderName)s changed the alternative addresses for this room.', {
|
||||
return () => _t('%(senderName)s changed the alternative addresses for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// both alias and alt_aliases where modified
|
||||
return _t('%(senderName)s changed the main and alternative addresses for this room.', {
|
||||
return () => _t('%(senderName)s changed the main and alternative addresses for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
// in case there is no difference between the two events,
|
||||
// say something as we can't simply hide the tile from here
|
||||
return _t('%(senderName)s changed the addresses for this room.', {
|
||||
return () => _t('%(senderName)s changed the addresses for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
|
||||
function textForCallAnswerEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
||||
function textForCallAnswerEvent(event): () => string | null {
|
||||
return () => {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
||||
};
|
||||
}
|
||||
|
||||
function textForCallHangupEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
function textForCallHangupEvent(event): () => string | null {
|
||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||
const eventContent = event.getContent();
|
||||
let reason = "";
|
||||
let getReason = () => "";
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
reason = _t('(not supported by this browser)');
|
||||
getReason = () => _t('(not supported by this browser)');
|
||||
} else if (eventContent.reason) {
|
||||
if (eventContent.reason === "ice_failed") {
|
||||
// We couldn't establish a connection at all
|
||||
reason = _t('(could not connect media)');
|
||||
getReason = () => _t('(could not connect media)');
|
||||
} else if (eventContent.reason === "ice_timeout") {
|
||||
// We established a connection but it died
|
||||
reason = _t('(connection failed)');
|
||||
getReason = () => _t('(connection failed)');
|
||||
} else if (eventContent.reason === "user_media_failed") {
|
||||
// The other side couldn't open capture devices
|
||||
reason = _t("(their device couldn't start the camera / microphone)");
|
||||
getReason = () => _t("(their device couldn't start the camera / microphone)");
|
||||
} else if (eventContent.reason === "unknown_error") {
|
||||
// An error code the other side doesn't have a way to express
|
||||
// (as opposed to an error code they gave but we don't know about,
|
||||
// in which case we show the error code)
|
||||
reason = _t("(an error occurred)");
|
||||
getReason = () => _t("(an error occurred)");
|
||||
} else if (eventContent.reason === "invite_timeout") {
|
||||
reason = _t('(no answer)');
|
||||
getReason = () => _t('(no answer)');
|
||||
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
|
||||
// workaround for https://github.com/vector-im/element-web/issues/5178
|
||||
// it seems Android randomly sets a reason of "user hangup" which is
|
||||
// interpreted as an error code :(
|
||||
// https://github.com/vector-im/riot-android/issues/2623
|
||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||
reason = '';
|
||||
getReason = () => '';
|
||||
} else {
|
||||
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
||||
getReason = () => _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
||||
}
|
||||
}
|
||||
return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason;
|
||||
return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
|
||||
}
|
||||
|
||||
function textForCallRejectEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
return _t('%(senderName)s declined the call.', {senderName});
|
||||
function textForCallRejectEvent(event): () => string | null {
|
||||
return () => {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
return _t('%(senderName)s declined the call.', {senderName});
|
||||
};
|
||||
}
|
||||
|
||||
function textForCallInviteEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
function textForCallInviteEvent(event): () => string | null {
|
||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||
// FIXME: Find a better way to determine this from the event?
|
||||
let isVoice = true;
|
||||
if (event.getContent().offer && event.getContent().offer.sdp &&
|
||||
|
@ -350,48 +365,55 @@ function textForCallInviteEvent(event) {
|
|||
// can have a hard time translating those strings. In an effort to make translations easier
|
||||
// and more accurate, we break out the string-based variables to a couple booleans.
|
||||
if (isVoice && isSupported) {
|
||||
return _t("%(senderName)s placed a voice call.", {senderName});
|
||||
return () => _t("%(senderName)s placed a voice call.", {
|
||||
senderName: getSenderName(),
|
||||
});
|
||||
} else if (isVoice && !isSupported) {
|
||||
return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName});
|
||||
return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
|
||||
senderName: getSenderName(),
|
||||
});
|
||||
} else if (!isVoice && isSupported) {
|
||||
return _t("%(senderName)s placed a video call.", {senderName});
|
||||
return () => _t("%(senderName)s placed a video call.", {
|
||||
senderName: getSenderName(),
|
||||
});
|
||||
} else if (!isVoice && !isSupported) {
|
||||
return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName});
|
||||
return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
|
||||
senderName: getSenderName(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function textForThreePidInviteEvent(event) {
|
||||
function textForThreePidInviteEvent(event): () => string | null {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
|
||||
if (!isValid3pidInvite(event)) {
|
||||
const targetDisplayName = event.getPrevContent().display_name || _t("Someone");
|
||||
return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
|
||||
return () => _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
|
||||
senderName,
|
||||
targetDisplayName,
|
||||
targetDisplayName: event.getPrevContent().display_name || _t("Someone"),
|
||||
});
|
||||
}
|
||||
|
||||
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||
return () => _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||
senderName,
|
||||
targetDisplayName: event.getContent().display_name,
|
||||
});
|
||||
}
|
||||
|
||||
function textForHistoryVisibilityEvent(event) {
|
||||
function textForHistoryVisibilityEvent(event): () => string | null {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
switch (event.getContent().history_visibility) {
|
||||
case 'invited':
|
||||
return _t('%(senderName)s made future room history visible to all room members, '
|
||||
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||
+ 'from the point they are invited.', {senderName});
|
||||
case 'joined':
|
||||
return _t('%(senderName)s made future room history visible to all room members, '
|
||||
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||
+ 'from the point they joined.', {senderName});
|
||||
case 'shared':
|
||||
return _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
||||
return () => _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
||||
case 'world_readable':
|
||||
return _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
||||
return () => _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
||||
default:
|
||||
return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
||||
return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
||||
senderName,
|
||||
visibility: event.getContent().history_visibility,
|
||||
});
|
||||
|
@ -399,11 +421,11 @@ function textForHistoryVisibilityEvent(event) {
|
|||
}
|
||||
|
||||
// Currently will only display a change if a user's power level is changed
|
||||
function textForPowerEvent(event) {
|
||||
function textForPowerEvent(event): () => string | null {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
if (!event.getPrevContent() || !event.getPrevContent().users ||
|
||||
!event.getContent() || !event.getContent().users) {
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
const userDefault = event.getContent().users_default || 0;
|
||||
// Construct set of userIds
|
||||
|
@ -418,38 +440,38 @@ function textForPowerEvent(event) {
|
|||
if (users.indexOf(userId) === -1) users.push(userId);
|
||||
},
|
||||
);
|
||||
const diff = [];
|
||||
// XXX: This is also surely broken for i18n
|
||||
const diffs = [];
|
||||
users.forEach((userId) => {
|
||||
// Previous power level
|
||||
const from = event.getPrevContent().users[userId];
|
||||
// Current power level
|
||||
const to = event.getContent().users[userId];
|
||||
if (to !== from) {
|
||||
diff.push(
|
||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||
userId,
|
||||
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
|
||||
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
|
||||
}),
|
||||
);
|
||||
diffs.push({ userId, from, to });
|
||||
}
|
||||
});
|
||||
if (!diff.length) {
|
||||
return '';
|
||||
if (!diffs.length) {
|
||||
return null;
|
||||
}
|
||||
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
||||
// XXX: This is also surely broken for i18n
|
||||
return () => _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
||||
senderName,
|
||||
powerLevelDiffText: diff.join(", "),
|
||||
powerLevelDiffText: diffs.map(diff =>
|
||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||
userId: diff.userId,
|
||||
fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault),
|
||||
toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault),
|
||||
}),
|
||||
).join(", "),
|
||||
});
|
||||
}
|
||||
|
||||
function textForPinnedEvent(event) {
|
||||
function textForPinnedEvent(event): () => string | null {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
||||
return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
||||
}
|
||||
|
||||
function textForWidgetEvent(event) {
|
||||
function textForWidgetEvent(event): () => string | null {
|
||||
const senderName = event.getSender();
|
||||
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
|
||||
const {name, type, url} = event.getContent() || {};
|
||||
|
@ -464,27 +486,27 @@ function textForWidgetEvent(event) {
|
|||
// equivalent to that condition.
|
||||
if (url) {
|
||||
if (prevUrl) {
|
||||
return _t('%(widgetName)s widget modified by %(senderName)s', {
|
||||
return () => _t('%(widgetName)s widget modified by %(senderName)s', {
|
||||
widgetName, senderName,
|
||||
});
|
||||
} else {
|
||||
return _t('%(widgetName)s widget added by %(senderName)s', {
|
||||
return () => _t('%(widgetName)s widget added by %(senderName)s', {
|
||||
widgetName, senderName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return _t('%(widgetName)s widget removed by %(senderName)s', {
|
||||
return () => _t('%(widgetName)s widget removed by %(senderName)s', {
|
||||
widgetName, senderName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function textForWidgetLayoutEvent(event) {
|
||||
function textForWidgetLayoutEvent(event): () => string | null {
|
||||
const senderName = event.sender?.name || event.getSender();
|
||||
return _t("%(senderName)s has updated the widget layout", {senderName});
|
||||
return () => _t("%(senderName)s has updated the widget layout", {senderName});
|
||||
}
|
||||
|
||||
function textForMjolnirEvent(event) {
|
||||
function textForMjolnirEvent(event): () => string | null {
|
||||
const senderName = event.getSender();
|
||||
const {entity: prevEntity} = event.getPrevContent();
|
||||
const {entity, recommendation, reason} = event.getContent();
|
||||
|
@ -492,74 +514,74 @@ function textForMjolnirEvent(event) {
|
|||
// Rule removed
|
||||
if (!entity) {
|
||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s removed the rule banning users matching %(glob)s",
|
||||
return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
|
||||
{senderName, glob: prevEntity});
|
||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
|
||||
return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
|
||||
{senderName, glob: prevEntity});
|
||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s removed the rule banning servers matching %(glob)s",
|
||||
return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
|
||||
{senderName, glob: prevEntity});
|
||||
}
|
||||
|
||||
// Unknown type. We'll say something, but we shouldn't end up here.
|
||||
return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
|
||||
return () => _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
|
||||
}
|
||||
|
||||
// Invalid rule
|
||||
if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName});
|
||||
if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, {senderName});
|
||||
|
||||
// Rule updated
|
||||
if (entity === prevEntity) {
|
||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
|
||||
return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
|
||||
return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
|
||||
return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
}
|
||||
|
||||
// Unknown type. We'll say something but we shouldn't end up here.
|
||||
return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
|
||||
return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
}
|
||||
|
||||
// New rule
|
||||
if (!prevEntity) {
|
||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
|
||||
return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
|
||||
return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
|
||||
return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
}
|
||||
|
||||
// Unknown type. We'll say something but we shouldn't end up here.
|
||||
return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
|
||||
return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
}
|
||||
|
||||
// else the entity !== prevEntity - count as a removal & add
|
||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t(
|
||||
return () => _t(
|
||||
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
||||
"%(newGlob)s for %(reason)s",
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||
);
|
||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||
return _t(
|
||||
return () => _t(
|
||||
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
||||
"%(newGlob)s for %(reason)s",
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||
);
|
||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t(
|
||||
return () => _t(
|
||||
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
||||
"%(newGlob)s for %(reason)s",
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||
|
@ -567,11 +589,15 @@ function textForMjolnirEvent(event) {
|
|||
}
|
||||
|
||||
// Unknown type. We'll say something but we shouldn't end up here.
|
||||
return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
|
||||
return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
|
||||
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
interface IHandlers {
|
||||
[type: string]: (ev: any) => (() => string | null);
|
||||
}
|
||||
|
||||
const handlers: IHandlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
'm.call.answer': textForCallAnswerEvent,
|
||||
|
@ -579,7 +605,7 @@ const handlers = {
|
|||
'm.call.reject': textForCallRejectEvent,
|
||||
};
|
||||
|
||||
const stateHandlers = {
|
||||
const stateHandlers: IHandlers = {
|
||||
'm.room.canonical_alias': textForCanonicalAliasEvent,
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
|
@ -604,8 +630,12 @@ for (const evType of ALL_RULE_TYPES) {
|
|||
stateHandlers[evType] = textForMjolnirEvent;
|
||||
}
|
||||
|
||||
export function textForEvent(ev) {
|
||||
export function hasText(ev): boolean {
|
||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||
if (handler) return handler(ev);
|
||||
return '';
|
||||
return Boolean(handler?.(ev));
|
||||
}
|
||||
|
||||
export function textForEvent(ev): string {
|
||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||
return handler?.(ev)?.() || '';
|
||||
}
|
|
@ -24,13 +24,16 @@ import { HostSignupStore } from "../../stores/HostSignupStore";
|
|||
import SdkConfig from "../../SdkConfig";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {}
|
||||
interface IProps {
|
||||
onClick?(): void;
|
||||
}
|
||||
|
||||
interface IState {}
|
||||
|
||||
@replaceableComponent("structures.HostSignupAction")
|
||||
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
||||
private openDialog = async () => {
|
||||
this.props.onClick?.();
|
||||
await HostSignupStore.instance.setHostSignupActive(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -439,6 +439,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
onBlur={this.onBlur}
|
||||
isMinimized={this.props.isMinimized}
|
||||
activeSpace={this.state.activeSpace}
|
||||
onResize={this.refreshStickyHeaders}
|
||||
onListCollapse={this.refreshStickyHeaders}
|
||||
/>;
|
||||
|
||||
|
|
|
@ -25,10 +25,11 @@ import * as sdk from '../../index';
|
|||
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import {Layout, LayoutPropType} from "../../settings/Layout";
|
||||
import {_t} from "../../languageHandler";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
import {hasText} from "../../TextForEvent";
|
||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||
import DMRoomMap from "../../utils/DMRoomMap";
|
||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
|
@ -151,6 +152,8 @@ export default class MessagePanel extends React.Component {
|
|||
enableFlair: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = RoomContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -380,7 +383,7 @@ export default class MessagePanel extends React.Component {
|
|||
// Always show highlighted event
|
||||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||
|
||||
return !shouldHideEvent(mxEv);
|
||||
return !shouldHideEvent(mxEv, this.context);
|
||||
}
|
||||
|
||||
_readMarkerForEvent(eventId, isLastEvent) {
|
||||
|
@ -1164,11 +1167,8 @@ class MemberGrouper {
|
|||
|
||||
add(ev) {
|
||||
if (ev.getType() === 'm.room.member') {
|
||||
// We'll just double check that it's worth our time to do so, through an
|
||||
// ugly hack. If textForEvent returns something, we should group it for
|
||||
// rendering but if it doesn't then we'll exclude it.
|
||||
const renderText = textForEvent(ev);
|
||||
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
|
||||
// We can ignore any events that don't actually have a message to display
|
||||
if (!hasText(ev)) return;
|
||||
}
|
||||
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
|
||||
ev.getId(),
|
||||
|
|
|
@ -50,6 +50,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
|||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
empty={emptyState}
|
||||
alwaysShowTimestamps={true}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -59,7 +59,6 @@ import ScrollPanel from "./ScrollPanel";
|
|||
import TimelinePanel from "./TimelinePanel";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||
import ForwardMessage from "../views/rooms/ForwardMessage";
|
||||
import SearchBar from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
|
@ -136,7 +135,6 @@ export interface IState {
|
|||
// Whether to highlight the event scrolled to
|
||||
isInitialEventHighlighted?: boolean;
|
||||
replyToEvent?: MatrixEvent;
|
||||
forwardingEvent?: MatrixEvent;
|
||||
numUnreadMessages: number;
|
||||
draggingFile: boolean;
|
||||
searching: boolean;
|
||||
|
@ -155,7 +153,6 @@ export interface IState {
|
|||
canPeek: boolean;
|
||||
showApps: boolean;
|
||||
isPeeking: boolean;
|
||||
showReadReceipts: boolean;
|
||||
showRightPanel: boolean;
|
||||
// error object, as from the matrix client/server API
|
||||
// If we failed to load information about the room,
|
||||
|
@ -183,6 +180,12 @@ export interface IState {
|
|||
canReact: boolean;
|
||||
canReply: boolean;
|
||||
layout: Layout;
|
||||
lowBandwidth: boolean;
|
||||
showReadReceipts: boolean;
|
||||
showRedactions: boolean;
|
||||
showJoinLeaves: boolean;
|
||||
showAvatarChanges: boolean;
|
||||
showDisplaynameChanges: boolean;
|
||||
matrixClientIsReady: boolean;
|
||||
showUrlPreview?: boolean;
|
||||
e2eStatus?: E2EStatus;
|
||||
|
@ -200,8 +203,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
private readonly dispatcherRef: string;
|
||||
private readonly roomStoreToken: EventSubscription;
|
||||
private readonly rightPanelStoreToken: EventSubscription;
|
||||
private readonly showReadReceiptsWatchRef: string;
|
||||
private readonly layoutWatcherRef: string;
|
||||
private settingWatchers: string[];
|
||||
|
||||
private unmounted = false;
|
||||
private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
|
||||
|
@ -232,7 +234,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
canPeek: false,
|
||||
showApps: false,
|
||||
isPeeking: false,
|
||||
showReadReceipts: true,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
joining: false,
|
||||
atEndOfLiveTimeline: true,
|
||||
|
@ -242,6 +243,12 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
canReact: false,
|
||||
canReply: false,
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||
showReadReceipts: true,
|
||||
showRedactions: true,
|
||||
showJoinLeaves: true,
|
||||
showAvatarChanges: true,
|
||||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
dragCounter: 0,
|
||||
};
|
||||
|
@ -268,9 +275,14 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
|
||||
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
||||
this.onReadReceiptsChange);
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
|
||||
this.settingWatchers = [
|
||||
SettingsStore.watchSetting("layout", null, () =>
|
||||
this.setState({ layout: SettingsStore.getValue("layout") }),
|
||||
),
|
||||
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
||||
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private onWidgetStoreUpdate = () => {
|
||||
|
@ -323,13 +335,45 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||
};
|
||||
|
||||
// Add watchers for each of the settings we just looked up
|
||||
this.settingWatchers = this.settingWatchers.concat([
|
||||
SettingsStore.watchSetting("showReadReceipts", null, () =>
|
||||
this.setState({
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
}),
|
||||
),
|
||||
SettingsStore.watchSetting("showRedactions", null, () =>
|
||||
this.setState({
|
||||
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||
}),
|
||||
),
|
||||
SettingsStore.watchSetting("showJoinLeaves", null, () =>
|
||||
this.setState({
|
||||
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||
}),
|
||||
),
|
||||
SettingsStore.watchSetting("showAvatarChanges", null, () =>
|
||||
this.setState({
|
||||
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||
}),
|
||||
),
|
||||
SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
|
||||
this.setState({
|
||||
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
||||
// Stop peeking because we have joined this room now
|
||||
this.context.stopPeeking();
|
||||
|
@ -638,10 +682,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.showReadReceiptsWatchRef) {
|
||||
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this.updateRoomMembers.cancelPendingCall();
|
||||
|
||||
|
@ -649,7 +689,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
// console.log("Tinter.tint from RoomView.unmount");
|
||||
// Tinter.tint(); // reset colourscheme
|
||||
|
||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
||||
for (const watcher of this.settingWatchers) {
|
||||
SettingsStore.unwatchSetting(watcher);
|
||||
}
|
||||
}
|
||||
|
||||
private onUserScroll = () => {
|
||||
|
@ -819,7 +861,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
// update unread count when scrolled up
|
||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||
// no change
|
||||
} else if (!shouldHideEvent(ev)) {
|
||||
} else if (!shouldHideEvent(ev, this.state)) {
|
||||
this.setState((state, props) => {
|
||||
return {numUnreadMessages: state.numUnreadMessages + 1};
|
||||
});
|
||||
|
@ -1410,18 +1452,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
dis.dispatch({ action: "open_room_settings" });
|
||||
};
|
||||
|
||||
private onCancelClick = () => {
|
||||
console.log("updateTint from onCancelClick");
|
||||
this.updateTint();
|
||||
if (this.state.forwardingEvent) {
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
event: null,
|
||||
});
|
||||
}
|
||||
dis.fire(Action.FocusComposer);
|
||||
};
|
||||
|
||||
private onAppsClick = () => {
|
||||
dis.dispatch({
|
||||
action: "appsDrawer",
|
||||
|
@ -1639,6 +1669,24 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* called by the parent component when PageUp/Down/etc is pressed.
|
||||
*
|
||||
* We pass it down to the scroll panel.
|
||||
*/
|
||||
private handleScrollKey = ev => {
|
||||
let panel;
|
||||
if (this.searchResultsPanel.current) {
|
||||
panel = this.searchResultsPanel.current;
|
||||
} else if (this.messagePanel) {
|
||||
panel = this.messagePanel;
|
||||
}
|
||||
|
||||
if (panel) {
|
||||
panel.handleScrollKey(ev);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* get any current call for this room
|
||||
*/
|
||||
|
@ -1837,11 +1885,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
let aux = null;
|
||||
let previewBar;
|
||||
let hideCancel = false;
|
||||
if (this.state.forwardingEvent) {
|
||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
||||
} else if (this.state.searching) {
|
||||
hideCancel = true; // has own cancel
|
||||
if (this.state.searching) {
|
||||
aux = <SearchBar
|
||||
searchInProgress={this.state.searchInProgress}
|
||||
onCancelClick={this.onCancelSearchClick}
|
||||
|
@ -1850,7 +1894,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
/>;
|
||||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||
hideCancel = true;
|
||||
} else if (myMembership !== "join") {
|
||||
// We do have a room object for this room, but we're not currently in it.
|
||||
// We may have a 3rd party invite to it.
|
||||
|
@ -1859,7 +1902,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
inviterName = this.props.oobData.inviterName;
|
||||
}
|
||||
const invitedEmail = this.props.threepidInvite?.toEmail;
|
||||
hideCancel = true;
|
||||
previewBar = (
|
||||
<RoomPreviewBar
|
||||
onJoinClick={this.onJoinButtonClicked}
|
||||
|
@ -1977,11 +2019,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
hideMessagePanel = true;
|
||||
}
|
||||
|
||||
const shouldHighlight = this.state.isInitialEventHighlighted;
|
||||
let highlightedEventId = null;
|
||||
if (this.state.forwardingEvent) {
|
||||
highlightedEventId = this.state.forwardingEvent.getId();
|
||||
} else if (shouldHighlight) {
|
||||
if (this.state.isInitialEventHighlighted) {
|
||||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
|
@ -2070,7 +2109,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
inRoom={myMembership === 'join'}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
|
|
|
@ -520,6 +520,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
setError("Failed to update some suggestions. Try again later");
|
||||
}
|
||||
setSaving(false);
|
||||
setSelected(new Map());
|
||||
}}
|
||||
kind="primary_outline"
|
||||
disabled={disabled}
|
||||
|
|
|
@ -28,7 +28,7 @@ import RoomTopic from "../views/elements/RoomTopic";
|
|||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
||||
import createRoom, {IOpts, Preset} from "../../createRoom";
|
||||
import createRoom, {IOpts} from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||
import withValidation from "../views/elements/Validation";
|
||||
|
@ -65,6 +65,7 @@ import dis from "../../dispatcher/dispatcher";
|
|||
import Modal from "../../Modal";
|
||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -587,6 +588,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
|
|||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
</AccessibleButton>
|
||||
<div className="mx_SpaceRoomView_betaWarning">
|
||||
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
|
||||
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
|
||||
</div>
|
||||
<SpaceFeedbackPrompt />
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ 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 RoomContext from "../../contexts/RoomContext";
|
||||
import UserActivity from "../../UserActivity";
|
||||
import Modal from "../../Modal";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
|
@ -120,8 +121,13 @@ class TimelinePanel extends React.Component {
|
|||
|
||||
// which layout to use
|
||||
layout: LayoutPropType,
|
||||
|
||||
// whether to always show timestamps for an event
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
}
|
||||
|
||||
static contextType = RoomContext;
|
||||
|
||||
// a map from room id to read marker event timestamp
|
||||
static roomReadMarkerTsMap = {};
|
||||
|
||||
|
@ -1285,7 +1291,7 @@ class TimelinePanel extends React.Component {
|
|||
|
||||
const shouldIgnore = !!ev.status || // local echo
|
||||
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
|
||||
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev);
|
||||
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
|
||||
|
||||
if (isWithoutTile || !node) {
|
||||
// don't start counting if the event should be ignored,
|
||||
|
@ -1440,7 +1446,7 @@ class TimelinePanel extends React.Component {
|
|||
onFillRequest={this.onMessageListFillRequest}
|
||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||
isTwelveHour={this.state.isTwelveHour}
|
||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
|
||||
className={this.props.className}
|
||||
tileShape={this.props.tileShape}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
|
|
|
@ -366,9 +366,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
const mxDomain = MatrixClientPeg.get().getDomain();
|
||||
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
||||
if (!hostSignupConfig.domains || validDomains.length > 0) {
|
||||
topSection = <div onClick={this.onCloseMenu}>
|
||||
<HostSignupAction />
|
||||
</div>;
|
||||
topSection = <HostSignupAction onClick={this.onCloseMenu} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import classNames from 'classnames';
|
|||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
|
@ -44,12 +45,12 @@ interface IProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
const calculateUrls = (url, urls) => {
|
||||
const calculateUrls = (url, urls, lowBandwidth) => {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, ...props.urls ]
|
||||
|
||||
let _urls = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
if (!lowBandwidth) {
|
||||
_urls = urls || [];
|
||||
|
||||
if (url) {
|
||||
|
@ -63,7 +64,13 @@ const calculateUrls = (url, urls) => {
|
|||
};
|
||||
|
||||
const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls));
|
||||
// Since this is a hot code path and the settings store can be slow, we
|
||||
// use the cached lowBandwidth value from the room context if it exists
|
||||
const roomContext = useContext(RoomContext);
|
||||
const lowBandwidth = roomContext ?
|
||||
roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
|
||||
|
||||
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
|
||||
const [urlsIndex, setIndex] = useState<number>(0);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
|
@ -71,7 +78,7 @@ const useImageUrl = ({url, urls}): [string, () => void] => {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setUrls(calculateUrls(url, urls));
|
||||
setUrls(calculateUrls(url, urls, lowBandwidth));
|
||||
setIndex(0);
|
||||
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import { MenuItem } from "../../structures/ContextMenu";
|
|||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||
|
||||
export function canCancel(eventStatus) {
|
||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
|
@ -157,10 +158,10 @@ export default class MessageContextMenu extends React.Component {
|
|||
};
|
||||
|
||||
onForwardClick = () => {
|
||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
event: this.props.mxEvent,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
});
|
||||
this.closeMenu();
|
||||
};
|
||||
|
|
|
@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
|||
showUnpin?: boolean;
|
||||
// override delete handler
|
||||
onDeleteClick?(): void;
|
||||
// override edit handler
|
||||
onEditClick?(): void;
|
||||
}
|
||||
|
||||
const WidgetContextMenu: React.FC<IProps> = ({
|
||||
|
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
app,
|
||||
userWidget,
|
||||
onDeleteClick,
|
||||
onEditClick,
|
||||
showUnpin,
|
||||
...props
|
||||
}) => {
|
||||
|
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
|
||||
let editButton;
|
||||
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
||||
const onEditClick = () => {
|
||||
WidgetUtils.editWidget(room, app);
|
||||
const _onEditClick = () => {
|
||||
if (onEditClick) {
|
||||
onEditClick();
|
||||
} else {
|
||||
WidgetUtils.editWidget(room, app);
|
||||
}
|
||||
onFinished();
|
||||
};
|
||||
|
||||
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />;
|
||||
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
|
||||
}
|
||||
|
||||
let snapshotButton;
|
||||
|
@ -116,24 +123,29 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
|
||||
let deleteButton;
|
||||
if (onDeleteClick || canModify) {
|
||||
const onDeleteClickDefault = () => {
|
||||
// Show delete confirmation dialog
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||
},
|
||||
});
|
||||
const _onDeleteClick = () => {
|
||||
if (onDeleteClick) {
|
||||
onDeleteClick();
|
||||
} else {
|
||||
// Show delete confirmation dialog
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onFinished();
|
||||
};
|
||||
|
||||
deleteButton = <IconizedContextMenuOption
|
||||
onClick={onDeleteClick || onDeleteClickDefault}
|
||||
onClick={_onDeleteClick}
|
||||
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -15,22 +15,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {ChangeEvent, createRef, KeyboardEvent, SyntheticEvent} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import withValidation, {IFieldState} from '../elements/Validation';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import withValidation, { IFieldState } from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field from "../elements/Field";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import BaseDialog from "../dialogs/BaseDialog";
|
||||
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
interface IProps {
|
||||
defaultPublic?: boolean;
|
||||
|
@ -72,7 +73,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
canChangeEncryption: true,
|
||||
};
|
||||
|
||||
MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
|
||||
MatrixClientPeg.get().doesServerForceEncryptionForPreset(Preset.PrivateChat)
|
||||
.then(isForced => this.setState({ canChangeEncryption: !isForced }));
|
||||
}
|
||||
|
||||
|
|
247
src/components/views/dialogs/ForwardDialog.tsx
Normal file
247
src/components/views/dialogs/ForwardDialog.tsx
Normal file
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||
|
||||
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, {useMemo, useState, useEffect} from "react";
|
||||
import classnames from "classnames";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {Layout} from "../../../settings/Layout";
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import {avatarUrlForUser} from "../../../Avatar";
|
||||
import EventTile from "../rooms/EventTile";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import {Alignment} from '../elements/Tooltip';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
|
||||
const AVATAR_SIZE = 30;
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
// The event to forward
|
||||
event: MatrixEvent;
|
||||
// We need a permalink creator for the source room to pass through to EventTile
|
||||
// in case the event is a reply (even though the user can't get at the link)
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
interface IEntryProps {
|
||||
room: Room;
|
||||
event: MatrixEvent;
|
||||
matrixClient: MatrixClient;
|
||||
onFinished(success: boolean): void;
|
||||
}
|
||||
|
||||
enum SendState {
|
||||
CanSend,
|
||||
Sending,
|
||||
Sent,
|
||||
Failed,
|
||||
}
|
||||
|
||||
const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
|
||||
const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
|
||||
|
||||
const jumpToRoom = () => {
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
onFinished(true);
|
||||
};
|
||||
const send = async () => {
|
||||
setSendState(SendState.Sending);
|
||||
try {
|
||||
await cli.sendEvent(room.roomId, event.getType(), event.getContent());
|
||||
setSendState(SendState.Sent);
|
||||
} catch (e) {
|
||||
setSendState(SendState.Failed);
|
||||
}
|
||||
};
|
||||
|
||||
let className;
|
||||
let disabled = false;
|
||||
let title;
|
||||
let icon;
|
||||
if (sendState === SendState.CanSend) {
|
||||
className = "mx_ForwardList_canSend";
|
||||
if (room.maySendMessage()) {
|
||||
title = _t("Send");
|
||||
} else {
|
||||
disabled = true;
|
||||
title = _t("You don't have permission to do this");
|
||||
}
|
||||
} else if (sendState === SendState.Sending) {
|
||||
className = "mx_ForwardList_sending";
|
||||
disabled = true;
|
||||
title = _t("Sending");
|
||||
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||
} else if (sendState === SendState.Sent) {
|
||||
className = "mx_ForwardList_sent";
|
||||
disabled = true;
|
||||
title = _t("Sent");
|
||||
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||
} else {
|
||||
className = "mx_ForwardList_sendFailed";
|
||||
disabled = true;
|
||||
title = _t("Failed to send");
|
||||
icon = <NotificationBadge
|
||||
notification={StaticNotificationState.RED_EXCLAMATION}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div className="mx_ForwardList_entry">
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ForwardList_roomButton"
|
||||
onClick={jumpToRoom}
|
||||
title={_t("Open link")}
|
||||
yOffset={-20}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
<DecoratedRoomAvatar room={room} avatarSize={32} />
|
||||
<span className="mx_ForwardList_entry_name">{ room.name }</span>
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
|
||||
className={`mx_ForwardList_sendButton ${className}`}
|
||||
onClick={send}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
yOffset={-20}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
|
||||
{ icon }
|
||||
</AccessibleTooltipButton>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
|
||||
const userId = cli.getUserId();
|
||||
const [profileInfo, setProfileInfo] = useState<any>({});
|
||||
useEffect(() => {
|
||||
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
|
||||
}, [cli, userId]);
|
||||
|
||||
// For the message preview we fake the sender as ourselves
|
||||
const mockEvent = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: userId,
|
||||
content: event.getContent(),
|
||||
unsigned: {
|
||||
age: 97,
|
||||
},
|
||||
event_id: "$9999999999999999999999999999999999999999999",
|
||||
room_id: event.getRoomId(),
|
||||
});
|
||||
mockEvent.sender = {
|
||||
name: profileInfo.displayname || userId,
|
||||
userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return avatarUrlForUser(
|
||||
{ avatarUrl: profileInfo.avatar_url },
|
||||
AVATAR_SIZE, AVATAR_SIZE, "crop",
|
||||
);
|
||||
},
|
||||
getMxcAvatarUrl: () => profileInfo.avatar_url,
|
||||
};
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
const spacesEnabled = useFeatureEnabled("feature_spaces");
|
||||
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
|
||||
const previewLayout = useSettingValue<Layout>("layout");
|
||||
|
||||
let rooms = useMemo(() => sortRooms(
|
||||
cli.getVisibleRooms().filter(
|
||||
room => room.getMyMembership() === "join" &&
|
||||
!(spacesEnabled && room.isSpaceRoom()),
|
||||
),
|
||||
), [cli, spacesEnabled]);
|
||||
|
||||
if (lcQuery) {
|
||||
rooms = new QueryMatcher<Room>(rooms, {
|
||||
keys: ["name"],
|
||||
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||
shouldMatchWordsOnly: false,
|
||||
}).match(lcQuery);
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Forward message")}
|
||||
className="mx_ForwardDialog"
|
||||
contentId="mx_ForwardList"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<h3>{ _t("Message preview") }</h3>
|
||||
<div className={classnames("mx_ForwardDialog_preview", {
|
||||
"mx_IRCLayout": previewLayout == Layout.IRC,
|
||||
"mx_GroupLayout": previewLayout == Layout.Group,
|
||||
})}>
|
||||
<EventTile
|
||||
mxEvent={mockEvent}
|
||||
layout={previewLayout}
|
||||
enableFlair={flairEnabled}
|
||||
permalinkCreator={permalinkCreator}
|
||||
as="div"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="mx_ForwardList" id="mx_ForwardList">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={_t("Search for rooms or people")}
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_ForwardList_results">
|
||||
{ rooms.map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
event={event}
|
||||
matrixClient={cli}
|
||||
onFinished={onFinished}
|
||||
/>,
|
||||
) }
|
||||
</div>
|
||||
) : <span className="mx_ForwardList_noResults">
|
||||
{ _t("No results") }
|
||||
</span> }
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default ForwardDialog;
|
|
@ -14,7 +14,9 @@ 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 classNames from 'classnames';
|
||||
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
@ -31,7 +33,6 @@ import Modal from "../../../Modal";
|
|||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {
|
||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||
IInvite3PID,
|
||||
} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
@ -50,6 +51,12 @@ import {getAddressType} from "../../../UserAddress";
|
|||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { compare } from '../../../utils/strings';
|
||||
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { copyPlaintext, selectText } from "../../../utils/strings";
|
||||
import * as ContextMenu from "../../structures/ContextMenu";
|
||||
import { toRightOf } from "../../structures/ContextMenu";
|
||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -351,6 +358,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
initialText: "",
|
||||
};
|
||||
|
||||
private closeCopiedTooltip: () => void;
|
||||
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
||||
private editorRef = createRef<HTMLInputElement>();
|
||||
private unmounted = false;
|
||||
|
@ -403,6 +411,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
|
||||
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
|
||||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||
}
|
||||
|
||||
private onConsultFirstChange = (ev) => {
|
||||
|
@ -1238,6 +1249,25 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
}
|
||||
|
||||
private async onLinkClick(e) {
|
||||
e.preventDefault();
|
||||
selectText(e.target);
|
||||
}
|
||||
|
||||
private onCopyClick = async e => {
|
||||
e.preventDefault();
|
||||
const target = e.target; // copy target before we go async and React throws it away
|
||||
|
||||
const successful = await copyPlaintext(makeUserPermalink(MatrixClientPeg.get().getUserId()));
|
||||
const buttonRect = target.getBoundingClientRect();
|
||||
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t("Copied!") : _t("Failed to copy"),
|
||||
});
|
||||
// Drop a reference to this close handler for componentWillUnmount
|
||||
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
@ -1248,12 +1278,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
spinner = <Spinner w={20} h={20} />;
|
||||
}
|
||||
|
||||
|
||||
let title;
|
||||
let helpText;
|
||||
let buttonText;
|
||||
let goButtonFn;
|
||||
let consultSection;
|
||||
let extraSection;
|
||||
let footer;
|
||||
let keySharingWarning = <span />;
|
||||
|
||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||
|
@ -1316,6 +1346,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this.startDm;
|
||||
extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
|
||||
<span>{ _t("Some suggestions may be hidden for privacy.") }</span>
|
||||
<p>{ _t("If you can't see who you’re looking for, send them your invite link below.") }</p>
|
||||
</div>;
|
||||
const link = makeUserPermalink(MatrixClientPeg.get().getUserId());
|
||||
footer = <div className="mx_InviteDialog_footer">
|
||||
<h3>{ _t("Or send invite link") }</h3>
|
||||
<div className="mx_InviteDialog_footer_link">
|
||||
<a href={link} onClick={this.onLinkClick}>
|
||||
{ link }
|
||||
</a>
|
||||
<AccessibleTooltipButton
|
||||
title={_t("Copy")}
|
||||
onClick={this.onCopyClick}
|
||||
className="mx_InviteDialog_footer_link_copy"
|
||||
>
|
||||
<div />
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
</div>
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||
|
@ -1377,7 +1427,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
title = _t("Transfer");
|
||||
buttonText = _t("Transfer");
|
||||
goButtonFn = this.transferCall;
|
||||
consultSection = <div>
|
||||
footer = <div>
|
||||
<label>
|
||||
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
||||
{_t("Consult first")}
|
||||
|
@ -1391,7 +1441,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_InviteDialog'
|
||||
className={classNames("mx_InviteDialog", {
|
||||
mx_InviteDialog_hasFooter: !!footer,
|
||||
})}
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
|
@ -1418,8 +1470,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
<div className='mx_InviteDialog_userSections'>
|
||||
{this.renderSection('recents')}
|
||||
{this.renderSection('suggestions')}
|
||||
{extraSection}
|
||||
</div>
|
||||
{consultSection}
|
||||
{footer}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TabbedView, {Tab} from "../../structures/TabbedView";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
||||
|
@ -39,31 +38,36 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
|
|||
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
|
||||
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onFinished: (success: boolean) => void;
|
||||
initialTabId?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
||||
export default class RoomSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
export default class RoomSettingsDialog extends React.Component<IProps> {
|
||||
private dispatcherRef: string;
|
||||
|
||||
componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
|
||||
public componentWillUnmount() {
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
private onAction = (payload): void => {
|
||||
// When view changes below us, close the room settings
|
||||
// whilst the modal is open this can only be triggered when someone hits Leave Room
|
||||
if (payload.action === 'view_home_page') {
|
||||
this.props.onFinished();
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
};
|
||||
|
||||
_getTabs() {
|
||||
const tabs = [];
|
||||
private getTabs(): Tab[] {
|
||||
const tabs: Tab[] = [];
|
||||
|
||||
tabs.push(new Tab(
|
||||
ROOM_GENERAL_TAB,
|
||||
|
@ -123,7 +127,10 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
title={_t("Room Settings - %(roomName)s", {roomName})}
|
||||
>
|
||||
<div className='mx_SettingsDialog_content'>
|
||||
<TabbedView tabs={this._getTabs()} />
|
||||
<TabbedView
|
||||
tabs={this.getTabs()}
|
||||
initialTabId={this.props.initialTabId}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip from './Tooltip';
|
||||
import Tooltip, {Alignment} from './Tooltip';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
|
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
|||
tooltipClassName?: string;
|
||||
forceHide?: boolean;
|
||||
yOffset?: number;
|
||||
alignment?: Alignment;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
|
||||
render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
|
||||
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
|
||||
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_AccessibleTooltipButton_container"
|
||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||
label={tooltip || title}
|
||||
yOffset={yOffset}
|
||||
alignment={alignment}
|
||||
/> : null;
|
||||
return (
|
||||
<AccessibleButton
|
||||
|
|
|
@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = getPersistKey(this.props.app.id);
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
try {
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
} catch (e) {
|
||||
console.log("Failed to construct widget", e);
|
||||
this._sgWidget = null;
|
||||
}
|
||||
this.iframe = null; // ref to the iframe (callback style)
|
||||
|
||||
this.state = this._getNewState(props);
|
||||
|
@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
|
|||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
this._sgWidget.stop();
|
||||
if (this._sgWidget) this._sgWidget.stop();
|
||||
}
|
||||
|
||||
this.setState({ hasPermissionToLoad });
|
||||
|
@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
// Only fetch IM token on mount if we're showing and have permission to load
|
||||
if (this.state.hasPermissionToLoad) {
|
||||
if (this._sgWidget && this.state.hasPermissionToLoad) {
|
||||
this._startWidget();
|
||||
}
|
||||
|
||||
|
@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
|
|||
if (this._sgWidget) {
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
this._sgWidget = new StopGapWidget(newProps);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this._startWidget();
|
||||
try {
|
||||
this._sgWidget = new StopGapWidget(newProps);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this._startWidget();
|
||||
} catch (e) {
|
||||
console.log("Failed to construct widget", e);
|
||||
this._sgWidget = null;
|
||||
}
|
||||
}
|
||||
|
||||
_startWidget() {
|
||||
|
@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
|
|||
_iframeRefChange = (ref) => {
|
||||
this.iframe = ref;
|
||||
if (ref) {
|
||||
this._sgWidget.start(ref);
|
||||
if (this._sgWidget) this._sgWidget.start(ref);
|
||||
} else {
|
||||
this._resetWidget(this.props);
|
||||
}
|
||||
|
@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
|
|||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
|
||||
this._sgWidget.stop({forceDestroy: true});
|
||||
if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
|
||||
}
|
||||
|
||||
_onWidgetPrepared = () => {
|
||||
|
@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
|
|||
<Spinner message={_t("Loading...")} />
|
||||
</div>
|
||||
);
|
||||
if (!this.state.hasPermissionToLoad) {
|
||||
if (this._sgWidget === null) {
|
||||
appTileBody = (
|
||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||
<AppWarning errorMsg={_t("Error loading Widget")} />
|
||||
</div>
|
||||
);
|
||||
} else if (!this.state.hasPermissionToLoad) {
|
||||
// only possible for room widgets, can assert this.props.room here
|
||||
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
appTileBody = (
|
||||
|
@ -364,7 +380,7 @@ export default class AppTile extends React.Component {
|
|||
if (this.isMixedContent()) {
|
||||
appTileBody = (
|
||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||
<AppWarning errorMsg="Error - Mixed content" />
|
||||
<AppWarning errorMsg={_t("Error - Mixed content")} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -417,6 +433,8 @@ export default class AppTile extends React.Component {
|
|||
onFinished={this._closeContextMenu}
|
||||
showUnpin={!this.props.userWidget}
|
||||
userWidget={this.props.userWidget}
|
||||
onEditClick={this.props.onEditClick}
|
||||
onDeleteClick={this.props.onDeleteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -128,6 +128,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
mxEvent={event}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
as="div"
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -19,20 +19,20 @@ limitations under the License.
|
|||
import React, { createRef } from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu';
|
||||
import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu';
|
||||
import MessageTimestamp from "../messages/MessageTimestamp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {formatFullDate} from "../../../DateUtils";
|
||||
import { formatFullDate } from "../../../DateUtils";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {normalizeWheelEvent} from "../../../utils/Mouse";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { normalizeWheelEvent } from "../../../utils/Mouse";
|
||||
|
||||
// Max scale to keep gaps around the image
|
||||
const MAX_SCALE = 0.95;
|
||||
|
@ -95,8 +95,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
private initX = 0;
|
||||
private initY = 0;
|
||||
private lastX = 0;
|
||||
private lastY = 0;
|
||||
private previousX = 0;
|
||||
private previousY = 0;
|
||||
|
||||
|
@ -105,23 +103,35 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
// needs to be passive in order to work with Chromium
|
||||
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
||||
// We want to recalculate zoom whenever the window's size changes
|
||||
window.addEventListener("resize", this.calculateZoom);
|
||||
window.addEventListener("resize", this.recalculateZoom);
|
||||
// After the image loads for the first time we want to calculate the zoom
|
||||
this.image.current.addEventListener("load", this.calculateZoom);
|
||||
this.image.current.addEventListener("load", this.recalculateZoom);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||
window.removeEventListener("resize", this.calculateZoom);
|
||||
this.image.current.removeEventListener("load", this.calculateZoom);
|
||||
window.removeEventListener("resize", this.recalculateZoom);
|
||||
this.image.current.removeEventListener("load", this.recalculateZoom);
|
||||
}
|
||||
|
||||
private calculateZoom = () => {
|
||||
private recalculateZoom = () => {
|
||||
this.setZoomAndRotation();
|
||||
}
|
||||
|
||||
private setZoomAndRotation = (inputRotation?: number) => {
|
||||
const image = this.image.current;
|
||||
const imageWrapper = this.imageWrapper.current;
|
||||
|
||||
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
|
||||
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
|
||||
const rotation = inputRotation || this.state.rotation;
|
||||
|
||||
const imageIsNotFlipped = rotation % 180 === 0;
|
||||
|
||||
// If the image is rotated take it into account
|
||||
const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight;
|
||||
const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth;
|
||||
|
||||
const zoomX = imageWrapper.clientWidth / width;
|
||||
const zoomY = imageWrapper.clientHeight / height;
|
||||
|
||||
// If the image is smaller in both dimensions set its the zoom to 1 to
|
||||
// display it in its original size
|
||||
|
@ -130,6 +140,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
zoom: 1,
|
||||
minZoom: 1,
|
||||
maxZoom: 1,
|
||||
rotation: rotation,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -138,10 +149,14 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
// image by default
|
||||
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
|
||||
|
||||
if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom});
|
||||
// If zoom is smaller than minZoom don't go below that value
|
||||
const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom;
|
||||
|
||||
this.setState({
|
||||
minZoom: minZoom,
|
||||
maxZoom: 1,
|
||||
rotation: rotation,
|
||||
zoom: zoom,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -157,7 +172,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
return;
|
||||
}
|
||||
if (newZoom >= this.state.maxZoom) {
|
||||
this.setState({zoom: this.state.maxZoom});
|
||||
this.setState({ zoom: this.state.maxZoom });
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -170,7 +185,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const {deltaY} = normalizeWheelEvent(ev);
|
||||
const { deltaY } = normalizeWheelEvent(ev);
|
||||
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
|
||||
};
|
||||
|
||||
|
@ -192,14 +207,12 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
private onRotateCounterClockwiseClick = () => {
|
||||
const cur = this.state.rotation;
|
||||
const rotationDegrees = cur - 90;
|
||||
this.setState({ rotation: rotationDegrees });
|
||||
this.setZoomAndRotation(cur - 90);
|
||||
};
|
||||
|
||||
private onRotateClockwiseClick = () => {
|
||||
const cur = this.state.rotation;
|
||||
const rotationDegrees = cur + 90;
|
||||
this.setState({ rotation: rotationDegrees });
|
||||
this.setZoomAndRotation(cur + 90);
|
||||
};
|
||||
|
||||
private onDownloadClick = () => {
|
||||
|
@ -246,15 +259,15 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
// Zoom in if we are completely zoomed out
|
||||
if (this.state.zoom === this.state.minZoom) {
|
||||
this.setState({zoom: this.state.maxZoom});
|
||||
this.setState({ zoom: this.state.maxZoom });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({moving: true});
|
||||
this.setState({ moving: true });
|
||||
this.previousX = this.state.translationX;
|
||||
this.previousY = this.state.translationY;
|
||||
this.initX = ev.pageX - this.lastX;
|
||||
this.initY = ev.pageY - this.lastY;
|
||||
this.initX = ev.pageX - this.state.translationX;
|
||||
this.initY = ev.pageY - this.state.translationY;
|
||||
};
|
||||
|
||||
private onMoving = (ev: React.MouseEvent) => {
|
||||
|
@ -263,11 +276,9 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
if (!this.state.moving) return;
|
||||
|
||||
this.lastX = ev.pageX - this.initX;
|
||||
this.lastY = ev.pageY - this.initY;
|
||||
this.setState({
|
||||
translationX: this.lastX,
|
||||
translationY: this.lastY,
|
||||
translationX: ev.pageX - this.initX,
|
||||
translationY: ev.pageY - this.initY,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -283,8 +294,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
translationX: 0,
|
||||
translationY: 0,
|
||||
});
|
||||
this.initX = 0;
|
||||
this.initY = 0;
|
||||
}
|
||||
this.setState({moving: false});
|
||||
this.setState({ moving: false });
|
||||
};
|
||||
|
||||
private renderContextMenu() {
|
||||
|
@ -355,7 +368,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const sender = (
|
||||
<div className="mx_ImageView_info_sender">
|
||||
{senderName}
|
||||
{ senderName }
|
||||
</div>
|
||||
);
|
||||
const messageTimestamp = (
|
||||
|
@ -382,10 +395,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
info = (
|
||||
<div className="mx_ImageView_info_wrapper">
|
||||
{avatar}
|
||||
{ avatar }
|
||||
<div className="mx_ImageView_info">
|
||||
{sender}
|
||||
{messageTimestamp}
|
||||
{ sender }
|
||||
{ messageTimestamp }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -425,7 +438,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||
title={_t("Zoom in")}
|
||||
onClick={ this.onZoomInClick }>
|
||||
onClick={this.onZoomInClick}>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
|
@ -441,7 +454,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
ref={this.focusLock}
|
||||
>
|
||||
<div className="mx_ImageView_panel">
|
||||
{info}
|
||||
{ info }
|
||||
<div className="mx_ImageView_toolbar">
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||
|
@ -453,20 +466,20 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
title={_t("Rotate Right")}
|
||||
onClick={this.onRotateClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
{zoomOutButton}
|
||||
{zoomInButton}
|
||||
{ zoomOutButton }
|
||||
{ zoomInButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
title={_t("Download")}
|
||||
onClick={ this.onDownloadClick }>
|
||||
</AccessibleTooltipButton>
|
||||
{contextMenuButton}
|
||||
{ contextMenuButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_close"
|
||||
title={_t("Close")}
|
||||
onClick={ this.props.onFinished }>
|
||||
</AccessibleTooltipButton>
|
||||
{this.renderContextMenu()}
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {forwardRef, ReactNode} from "react";
|
||||
import React, {forwardRef, ReactNode, ReactChildren} from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface IProps {
|
||||
className: string;
|
||||
title: string;
|
||||
subtitle?: ReactNode;
|
||||
children?: ReactChildren;
|
||||
}
|
||||
|
||||
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({ className, title, subtitle, children }, ref) => {
|
||||
|
|
|
@ -15,24 +15,31 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
onClick(): void;
|
||||
enableFlair: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
userGroups;
|
||||
relatedGroups;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.SenderProfile")
|
||||
export default class SenderProfile extends React.Component {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default class SenderProfile extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
private unmounted: boolean;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
const senderId = this.props.mxEvent.getSender();
|
||||
|
||||
this.state = {
|
||||
|
@ -40,6 +47,7 @@ export default class SenderProfile extends React.Component {
|
|||
relatedGroups: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this._updateRelatedGroups();
|
||||
|
@ -100,14 +108,26 @@ export default class SenderProfile extends React.Component {
|
|||
render() {
|
||||
const {mxEvent} = this.props;
|
||||
const colorClass = getUserNameColorClass(mxEvent.getSender());
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const {msgtype} = mxEvent.getContent();
|
||||
|
||||
const disambiguate = mxEvent.sender?.disambiguate;
|
||||
const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || "";
|
||||
const mxid = mxEvent.sender?.userId || mxEvent.getSender() || "";
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return null; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
|
||||
let flair = null;
|
||||
let mxidElement;
|
||||
if (disambiguate) {
|
||||
mxidElement = (
|
||||
<span className="mx_SenderProfile_mxid">
|
||||
{ mxid }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let flair;
|
||||
if (this.props.enableFlair) {
|
||||
const displayedGroups = this._getDisplayedGroups(
|
||||
this.state.userGroups, this.state.relatedGroups,
|
||||
|
@ -119,13 +139,12 @@ export default class SenderProfile extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
const nameElem = name || '';
|
||||
|
||||
return (
|
||||
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
|
||||
<span className={`mx_SenderProfile_name ${colorClass}`}>
|
||||
{ nameElem }
|
||||
<span className={`mx_SenderProfile_displayName ${colorClass}`}>
|
||||
{ displayName }
|
||||
</span>
|
||||
{ mxidElement }
|
||||
{ flair }
|
||||
</div>
|
||||
);
|
|
@ -137,12 +137,24 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
|||
|
||||
// show them in reverse, with latest pinned at the top
|
||||
content = pinnedEvents.filter(Boolean).reverse().map(ev => (
|
||||
<PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={onUnpinClicked} />
|
||||
<PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={() => onUnpinClicked(ev)} />
|
||||
));
|
||||
} else {
|
||||
content = <div className="mx_RightPanel_empty mx_PinnedMessagesCard_empty">
|
||||
<h2>{_t("You’re all caught up")}</h2>
|
||||
<p>{_t("You have no visible notifications.")}</p>
|
||||
content = <div className="mx_PinnedMessagesCard_empty">
|
||||
<div>
|
||||
{ /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
|
||||
<div className="mx_PinnedMessagesCard_MessageActionBar">
|
||||
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
|
||||
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" />
|
||||
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" />
|
||||
</div>
|
||||
|
||||
<h2>{ _t("Nothing pinned, yet") }</h2>
|
||||
{ _t("If you have permissions, open the menu on any message and select " +
|
||||
"<b>Pin</b> to stick them here.", {}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
}) }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|||
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import { hasText } from "../../../TextForEvent";
|
||||
import * as sdk from "../../../index";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -644,7 +644,18 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
// return early if there are no read receipts
|
||||
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
||||
return null;
|
||||
// We currently must include `mx_EventTile_readAvatars` in the DOM
|
||||
// of all events, as it is the positioned parent of the animated
|
||||
// read receipts. We can't let it unmount when a receipt moves
|
||||
// events, so for now we mount it for all events. Without it, the
|
||||
// animation will start from the top of the timeline (because it
|
||||
// lost its container).
|
||||
// See also https://github.com/vector-im/element-web/issues/17561
|
||||
return (
|
||||
<div className="mx_EventTile_msgOption">
|
||||
<span className="mx_EventTile_readAvatars" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||
|
@ -652,11 +663,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
const receiptOffset = 15;
|
||||
let left = 0;
|
||||
|
||||
const receipts = this.props.readReceipts || [];
|
||||
|
||||
if (receipts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const receipts = this.props.readReceipts;
|
||||
|
||||
for (let i = 0; i < receipts.length; ++i) {
|
||||
const receipt = receipts[i];
|
||||
|
@ -1052,58 +1059,65 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<RoomAvatar room={room} width={28} height={28} />
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
}, [
|
||||
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
|
||||
<RoomAvatar room={room} width={28} height={28} />
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</div>,
|
||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>,
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>
|
||||
</div>,
|
||||
]);
|
||||
}
|
||||
case 'file_grid': {
|
||||
return (
|
||||
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
}, [
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>
|
||||
</div>,
|
||||
<a
|
||||
className="mx_EventTile_senderDetailsLink"
|
||||
key="mx_EventTile_senderDetailsLink"
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</div>
|
||||
<a
|
||||
className="mx_EventTile_senderDetailsLink"
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
</a>,
|
||||
]);
|
||||
}
|
||||
|
||||
case 'reply':
|
||||
|
@ -1119,27 +1133,30 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
this.props.alwaysShowTimestamps || this.state.hover,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
|
||||
{ ircTimestamp }
|
||||
{ avatar }
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
<div className="mx_EventTile_reply">
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
showUrlPreview={false}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
}, [
|
||||
ircTimestamp,
|
||||
avatar,
|
||||
sender,
|
||||
ircPadlock,
|
||||
<div className="mx_EventTile_reply" key="mx_EventTile_reply">
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
showUrlPreview={false}
|
||||
/>
|
||||
</div>,
|
||||
]);
|
||||
}
|
||||
default: {
|
||||
const thread = ReplyThread.makeThread(
|
||||
|
@ -1210,7 +1227,7 @@ export function haveTileForEvent(e) {
|
|||
const handler = getHandlerTile(e);
|
||||
if (handler === undefined) return false;
|
||||
if (handler === 'messages.TextualEvent') {
|
||||
return TextForEvent.textForEvent(e) !== '';
|
||||
return hasText(e);
|
||||
} else if (handler === 'messages.RoomCreate') {
|
||||
return Boolean(e.getContent()['predecessor']);
|
||||
} else {
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 Michael Telatynski
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {Key} from '../../../Keyboard';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.ForwardMessage")
|
||||
export default class ForwardMessage extends React.Component {
|
||||
static propTypes = {
|
||||
onCancelClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
}
|
||||
|
||||
_onKeyDown = ev => {
|
||||
switch (ev.key) {
|
||||
case Key.ESCAPE:
|
||||
this.props.onCancelClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_ForwardMessage">
|
||||
<h1>{ _t('Please select the destination room for this message') }</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -31,6 +31,17 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import {showSpaceInvite} from "../../../utils/space";
|
||||
|
||||
import { privateShouldBeEncrypted } from "../../../createRoom";
|
||||
|
||||
import EventTileBubble from "../messages/EventTileBubble";
|
||||
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
|
||||
|
||||
function hasExpectedEncryptionSettings(room): boolean {
|
||||
const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId);
|
||||
const isPublic: boolean = room.getJoinRule() === "public";
|
||||
return isPublic || !privateShouldBeEncrypted() || isEncrypted;
|
||||
}
|
||||
|
||||
const NewRoomIntro = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const {room, roomId} = useContext(RoomContext);
|
||||
|
@ -166,7 +177,31 @@ const NewRoomIntro = () => {
|
|||
</React.Fragment>;
|
||||
}
|
||||
|
||||
function openRoomSettings(event) {
|
||||
event.preventDefault();
|
||||
dis.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: ROOM_SECURITY_TAB,
|
||||
});
|
||||
}
|
||||
|
||||
const sub2 = _t(
|
||||
"Your private messages are normally encrypted, but this room isn't. "+
|
||||
"Usually this is due to an unsupported device or method being used, " +
|
||||
"like email invites. <a>Enable encryption in settings.</a>", {},
|
||||
{ a: sub => <a onClick={openRoomSettings} href="#">{sub}</a> },
|
||||
);
|
||||
|
||||
return <div className="mx_NewRoomIntro">
|
||||
|
||||
{ !hasExpectedEncryptionSettings(room) && (
|
||||
<EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||
title={_t("End-to-end encryption isn't enabled")}
|
||||
subtitle={sub2}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ body }
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -95,6 +95,7 @@ export default class ReplyPreview extends React.Component {
|
|||
permalinkCreator={this.props.permalinkCreator}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
as="div"
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler';
|
|||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
import { CancelButton } from './SimpleRoomHeader';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
@ -42,7 +41,6 @@ export default class RoomHeader extends React.Component {
|
|||
onSettingsClick: PropTypes.func,
|
||||
onSearchClick: PropTypes.func,
|
||||
onLeaveClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
e2eStatus: PropTypes.string,
|
||||
onAppsClick: PropTypes.func,
|
||||
appsShown: PropTypes.bool,
|
||||
|
@ -52,7 +50,6 @@ export default class RoomHeader extends React.Component {
|
|||
static defaultProps = {
|
||||
editing: false,
|
||||
inRoom: false,
|
||||
onCancelClick: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -83,11 +80,6 @@ export default class RoomHeader extends React.Component {
|
|||
|
||||
render() {
|
||||
let searchStatus = null;
|
||||
let cancelButton = null;
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
|
@ -207,7 +199,6 @@ export default class RoomHeader extends React.Component {
|
|||
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
||||
{ name }
|
||||
{ topicElement }
|
||||
{ cancelButton }
|
||||
{ rightRow }
|
||||
<RoomHeaderButtons room={this.props.room} />
|
||||
</div>
|
||||
|
|
|
@ -55,6 +55,7 @@ interface IProps {
|
|||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
onFocus: (ev: React.FocusEvent) => void;
|
||||
onBlur: (ev: React.FocusEvent) => void;
|
||||
onResize: () => void;
|
||||
onListCollapse?: (isExpanded: boolean) => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
isMinimized: boolean;
|
||||
|
@ -404,7 +405,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
const newSublists = objectWithOnly(newLists, newListIds);
|
||||
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
||||
|
||||
this.setState({sublists, isNameFiltering});
|
||||
this.setState({sublists, isNameFiltering}, () => {
|
||||
this.props.onResize();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -454,8 +454,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const sublist = possibleSticky.parentElement.parentElement;
|
||||
const list = sublist.parentElement.parentElement;
|
||||
// the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky
|
||||
const isAtTop = list.scrollTop <= HEADER_HEIGHT;
|
||||
const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight;
|
||||
const listScrollTop = Math.round(list.scrollTop);
|
||||
const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT);
|
||||
const isAtBottom = listScrollTop >= Math.round(list.scrollHeight - list.offsetHeight);
|
||||
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
|
||||
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');
|
||||
|
||||
|
|
|
@ -16,23 +16,9 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
// cancel button which is shared between room header and simple room header
|
||||
export function CancelButton(props) {
|
||||
const {onClick} = props;
|
||||
|
||||
return (
|
||||
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
|
||||
<img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
|
||||
width="18" height="18" alt={_t("Cancel")} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* A stripped-down room header used for things like the user settings
|
||||
* and room directory.
|
||||
|
@ -41,18 +27,13 @@ export function CancelButton(props) {
|
|||
export default class SimpleRoomHeader extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
onCancelClick: PropTypes.func,
|
||||
|
||||
// `src` to a TintableSvg. Optional.
|
||||
icon: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
let cancelButton;
|
||||
let icon;
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
if (this.props.icon) {
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
icon = <TintableSvg
|
||||
|
@ -66,7 +47,6 @@ export default class SimpleRoomHeader extends React.Component {
|
|||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ icon }
|
||||
{ this.props.title }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent {
|
|||
/**
|
||||
* Launch the integration manager on the stickers integration page
|
||||
*/
|
||||
_launchManageIntegrations() {
|
||||
_launchManageIntegrations = () => {
|
||||
// TODO: Open the right integration manager for the widget
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(
|
||||
|
@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent {
|
|||
this.state.widgetId,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let stickerPicker;
|
||||
|
@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent {
|
|||
key="controls_hide_stickers"
|
||||
className={className}
|
||||
onClick={this._onHideStickersClick}
|
||||
active={this.state.showStickers}
|
||||
active={this.state.showStickers.toString()}
|
||||
title={_t("Hide Stickers")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
|
|
|
@ -22,7 +22,7 @@ import FocusLock from "react-focus-lock";
|
|||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
|
||||
import createRoom, {IStateEvent, Preset} from "../../../createRoom";
|
||||
import createRoom from "../../../createRoom";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {SpaceAvatar} from "./SpaceBasicSettings";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
@ -33,6 +33,8 @@ import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
|
|||
import Field from "../elements/Field";
|
||||
import withValidation from "../elements/Validation";
|
||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
|
||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||
|
@ -96,7 +98,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const initialState: IStateEvent[] = [
|
||||
const initialState: ICreateRoomStateEvent[] = [
|
||||
{
|
||||
type: EventType.RoomHistoryVisibility,
|
||||
content: {
|
||||
|
|
|
@ -31,7 +31,6 @@ const RoomContext = createContext<IState>({
|
|||
canPeek: false,
|
||||
showApps: false,
|
||||
isPeeking: false,
|
||||
showReadReceipts: true,
|
||||
showRightPanel: true,
|
||||
joining: false,
|
||||
atEndOfLiveTimeline: true,
|
||||
|
@ -41,6 +40,12 @@ const RoomContext = createContext<IState>({
|
|||
canReact: false,
|
||||
canReply: false,
|
||||
layout: Layout.Group,
|
||||
lowBandwidth: false,
|
||||
showReadReceipts: true,
|
||||
showRedactions: true,
|
||||
showJoinLeaves: true,
|
||||
showAvatarChanges: true,
|
||||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: false,
|
||||
dragCounter: 0,
|
||||
});
|
||||
|
|
|
@ -35,53 +35,15 @@ import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
|
|||
import SpaceStore from "./stores/SpaceStore";
|
||||
import { makeSpaceParentEvent } from "./utils/space";
|
||||
import { Action } from "./dispatcher/actions"
|
||||
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
// we define a number of interfaces which take their names from the js-sdk
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
|
||||
export enum Visibility {
|
||||
Public = "public",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
export enum Preset {
|
||||
PrivateChat = "private_chat",
|
||||
TrustedPrivateChat = "trusted_private_chat",
|
||||
PublicChat = "public_chat",
|
||||
}
|
||||
|
||||
interface Invite3PID {
|
||||
id_server: string;
|
||||
id_access_token?: string; // this gets injected by the js-sdk
|
||||
medium: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface IStateEvent {
|
||||
type: string;
|
||||
state_key?: string; // defaults to an empty string
|
||||
content: object;
|
||||
}
|
||||
|
||||
interface ICreateOpts {
|
||||
visibility?: Visibility;
|
||||
room_alias_name?: string;
|
||||
name?: string;
|
||||
topic?: string;
|
||||
invite?: string[];
|
||||
invite_3pid?: Invite3PID[];
|
||||
room_version?: string;
|
||||
creation_content?: object;
|
||||
initial_state?: IStateEvent[];
|
||||
preset?: Preset;
|
||||
is_direct?: boolean;
|
||||
power_level_content_override?: object;
|
||||
}
|
||||
|
||||
export interface IOpts {
|
||||
dmUserId?: string;
|
||||
createOpts?: ICreateOpts;
|
||||
createOpts?: ICreateRoomOpts;
|
||||
spinner?: boolean;
|
||||
guestAccess?: boolean;
|
||||
encryption?: boolean;
|
||||
|
@ -91,12 +53,6 @@ export interface IOpts {
|
|||
parentSpace?: Room;
|
||||
}
|
||||
|
||||
export interface IInvite3PID {
|
||||
id_server: string,
|
||||
medium: 'email',
|
||||
address: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new room, and switch to it.
|
||||
*
|
||||
|
@ -136,7 +92,7 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
|
|||
const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat;
|
||||
|
||||
// set some defaults for the creation
|
||||
const createOpts = opts.createOpts || {};
|
||||
const createOpts: ICreateRoomOpts = opts.createOpts || {};
|
||||
createOpts.preset = createOpts.preset || defaultPreset;
|
||||
createOpts.visibility = createOpts.visibility || Visibility.Private;
|
||||
if (opts.dmUserId && createOpts.invite === undefined) {
|
||||
|
|
|
@ -35,8 +35,8 @@ export const useSettingValue = <T>(settingName: string, roomId: string = null, e
|
|||
};
|
||||
|
||||
// Hook to fetch whether a feature is enabled and dynamically update when that changes
|
||||
export const useFeatureEnabled = (featureName: string, roomId: string = null) => {
|
||||
const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId));
|
||||
export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => {
|
||||
const [enabled, setEnabled] = useState(SettingsStore.getValue<boolean>(featureName, roomId));
|
||||
|
||||
useEffect(() => {
|
||||
const ref = SettingsStore.watchSetting(featureName, roomId, () => {
|
||||
|
|
|
@ -556,8 +556,8 @@
|
|||
"%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.",
|
||||
"%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.",
|
||||
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).",
|
||||
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
|
||||
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
|
||||
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
|
||||
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
|
||||
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
|
||||
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
|
||||
|
@ -1495,7 +1495,6 @@
|
|||
"Encrypting your message...": "Encrypting your message...",
|
||||
"Your message was sent": "Your message was sent",
|
||||
"Failed to send": "Failed to send",
|
||||
"Please select the destination room for this message": "Please select the destination room for this message",
|
||||
"Scroll to most recent messages": "Scroll to most recent messages",
|
||||
"Close preview": "Close preview",
|
||||
"and %(count)s others...|other": "and %(count)s others...",
|
||||
|
@ -1532,6 +1531,8 @@
|
|||
"Invite to just this room": "Invite to just this room",
|
||||
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
|
||||
"This is the start of <roomName/>.": "This is the start of <roomName/>.",
|
||||
"Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. <a>Enable encryption in settings.</a>": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. <a>Enable encryption in settings.</a>",
|
||||
"End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled",
|
||||
"Unpin": "Unpin",
|
||||
"View message": "View message",
|
||||
"%(duration)ss": "%(duration)ss",
|
||||
|
@ -1743,8 +1744,8 @@
|
|||
"The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to",
|
||||
"Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection",
|
||||
"Yours, or the other users’ session": "Yours, or the other users’ session",
|
||||
"You’re all caught up": "You’re all caught up",
|
||||
"You have no visible notifications.": "You have no visible notifications.",
|
||||
"Nothing pinned, yet": "Nothing pinned, yet",
|
||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||
"Pinned messages": "Pinned messages",
|
||||
"Room Info": "Room Info",
|
||||
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||
|
@ -1952,6 +1953,8 @@
|
|||
"Widgets do not use message encryption.": "Widgets do not use message encryption.",
|
||||
"Widget added by": "Widget added by",
|
||||
"This widget may use cookies.": "This widget may use cookies.",
|
||||
"Error loading Widget": "Error loading Widget",
|
||||
"Error - Mixed content": "Error - Mixed content",
|
||||
"Popout widget": "Popout widget",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
|
||||
|
@ -2230,6 +2233,13 @@
|
|||
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
|
||||
"Report a bug": "Report a bug",
|
||||
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
|
||||
"You don't have permission to do this": "You don't have permission to do this",
|
||||
"Sending": "Sending",
|
||||
"Sent": "Sent",
|
||||
"Open link": "Open link",
|
||||
"Forward message": "Forward message",
|
||||
"Message preview": "Message preview",
|
||||
"Search for rooms or people": "Search for rooms or people",
|
||||
"Confirm abort of host creation": "Confirm abort of host creation",
|
||||
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
|
||||
"Abort": "Abort",
|
||||
|
@ -2273,6 +2283,9 @@
|
|||
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
|
||||
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
|
||||
"Go": "Go",
|
||||
"Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.",
|
||||
"If you can't see who you’re looking for, send them your invite link below.": "If you can't see who you’re looking for, send them your invite link below.",
|
||||
"Or send invite link": "Or send invite link",
|
||||
"Unnamed Space": "Unnamed Space",
|
||||
"Invite to %(roomName)s": "Invite to %(roomName)s",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
|
||||
|
@ -2648,6 +2661,8 @@
|
|||
"Create a new community": "Create a new community",
|
||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
||||
"Communities are changing to Spaces": "Communities are changing to Spaces",
|
||||
"You’re all caught up": "You’re all caught up",
|
||||
"You have no visible notifications.": "You have no visible notifications.",
|
||||
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
|
||||
"%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.",
|
||||
"The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.",
|
||||
|
@ -2682,7 +2697,6 @@
|
|||
"Some of your messages have not been sent": "Some of your messages have not been sent",
|
||||
"Delete all": "Delete all",
|
||||
"Retry all": "Retry all",
|
||||
"Sending": "Sending",
|
||||
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
|
||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||
|
@ -2743,6 +2757,8 @@
|
|||
"A private space to organise your rooms": "A private space to organise your rooms",
|
||||
"Me and my teammates": "Me and my teammates",
|
||||
"A private space for you and your teammates": "A private space for you and your teammates",
|
||||
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
|
||||
"We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.",
|
||||
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
|
||||
"Inviting...": "Inviting...",
|
||||
"Invite your teammates": "Invite your teammates",
|
||||
|
|
|
@ -20,6 +20,7 @@ import SettingsStore from "../settings/SettingsStore";
|
|||
import {_t} from "../languageHandler";
|
||||
import dis from "../dispatcher/dispatcher";
|
||||
import {SettingLevel} from "../settings/SettingLevel";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
// TODO: Move this and related files to the js-sdk or something once finalized.
|
||||
|
||||
|
@ -86,7 +87,7 @@ export class Mjolnir {
|
|||
const resp = await MatrixClientPeg.get().createRoom({
|
||||
name: _t("My Ban List"),
|
||||
topic: _t("This is your list of users/servers you have blocked - don't leave the room!"),
|
||||
preset: "private_chat",
|
||||
preset: Preset.PrivateChat,
|
||||
});
|
||||
personalRoomId = resp['room_id'];
|
||||
await SettingsStore.setValue(
|
||||
|
|
|
@ -25,14 +25,8 @@ import Tar from "tar-js";
|
|||
|
||||
import * as rageshake from './rageshake';
|
||||
|
||||
// polyfill textencoder if necessary
|
||||
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
let TextEncoder = window.TextEncoder;
|
||||
if (!TextEncoder) {
|
||||
TextEncoder = TextEncodingUtf8.TextEncoder;
|
||||
}
|
||||
|
||||
interface IOpts {
|
||||
label?: string;
|
||||
|
|
|
@ -63,8 +63,7 @@ export class WatchManager {
|
|||
|
||||
if (!inRoomId) {
|
||||
// Fire updates to all the individual room watchers too, as they probably care about the change higher up.
|
||||
const callbacks = Array.from(roomWatchers.values()).flat(1);
|
||||
callbacks.push(...callbacks);
|
||||
callbacks.push(...Array.from(roomWatchers.values()).flat(1));
|
||||
} else if (roomWatchers.has(IRRELEVANT_ROOM)) {
|
||||
callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM));
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {IState} from "./components/structures/RoomView";
|
||||
|
||||
interface IDiff {
|
||||
isMemberEvent: boolean;
|
||||
|
@ -47,11 +48,18 @@ function memberEventDiff(ev: MatrixEvent): IDiff {
|
|||
return diff;
|
||||
}
|
||||
|
||||
export default function shouldHideEvent(ev: MatrixEvent): boolean {
|
||||
// Wrap getValue() for readability. Calling the SettingsStore can be
|
||||
// fairly resource heavy, so the checks below should avoid hitting it
|
||||
// where possible.
|
||||
const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId());
|
||||
/**
|
||||
* Determines whether the given event should be hidden from timelines.
|
||||
* @param ev The event
|
||||
* @param ctx An optional RoomContext to pull cached settings values from to avoid
|
||||
* hitting the settings store
|
||||
*/
|
||||
export default function shouldHideEvent(ev: MatrixEvent, ctx?: IState): boolean {
|
||||
// Accessing the settings store directly can be expensive if done frequently,
|
||||
// so we should prefer using cached values if a RoomContext is available
|
||||
const isEnabled = ctx ?
|
||||
name => ctx[name] :
|
||||
name => SettingsStore.getValue(name, ev.getRoomId());
|
||||
|
||||
// Hide redacted events
|
||||
if (ev.isRedacted() && !isEnabled('showRedactions')) return true;
|
||||
|
|
|
@ -54,8 +54,6 @@ const INITIAL_STATE = {
|
|||
// Any error that has occurred during loading
|
||||
roomLoadError: null,
|
||||
|
||||
forwardingEvent: null,
|
||||
|
||||
quotingEvent: null,
|
||||
|
||||
replyingToEvent: null,
|
||||
|
@ -150,11 +148,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
case 'on_logged_out':
|
||||
this.reset();
|
||||
break;
|
||||
case 'forward_event':
|
||||
this.setState({
|
||||
forwardingEvent: payload.event,
|
||||
});
|
||||
break;
|
||||
case 'reply_to_event':
|
||||
// If currently viewed room does not match the room in which we wish to reply then change rooms
|
||||
// this can happen when performing a search across all rooms
|
||||
|
@ -174,6 +167,7 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
|
||||
Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
|
||||
roomId: payload.room_id || this.state.roomId,
|
||||
initialTabId: payload.initial_tab_id,
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
break;
|
||||
}
|
||||
|
@ -187,7 +181,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
roomAlias: payload.room_alias,
|
||||
initialEventId: payload.event_id,
|
||||
isInitialEventHighlighted: payload.highlighted,
|
||||
forwardingEvent: null,
|
||||
roomLoading: false,
|
||||
roomLoadError: null,
|
||||
// should peek by default
|
||||
|
@ -207,14 +200,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
newState.replyingToEvent = payload.replyingToEvent;
|
||||
}
|
||||
|
||||
if (this.state.forwardingEvent) {
|
||||
dis.dispatch({
|
||||
action: 'send_event',
|
||||
room_id: newState.roomId,
|
||||
event: this.state.forwardingEvent,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
if (payload.auto_join) {
|
||||
|
@ -428,11 +413,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
return this.state.joinError;
|
||||
}
|
||||
|
||||
// The mxEvent if one is about to be forwarded
|
||||
public getForwardingEvent() {
|
||||
return this.state.forwardingEvent;
|
||||
}
|
||||
|
||||
// The mxEvent if one is currently being replied to/quoted
|
||||
public getQuotingEvent() {
|
||||
return this.state.replyingToEvent;
|
||||
|
|
|
@ -332,7 +332,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||
}
|
||||
|
||||
public getContainerWidgets(room: Room, container: Container): IApp[] {
|
||||
return this.byRoom[room.roomId]?.[container]?.ordered || [];
|
||||
return this.byRoom[room?.roomId]?.[container]?.ordered || [];
|
||||
}
|
||||
|
||||
public isInContainer(room: Room, widget: IApp, container: Container): boolean {
|
||||
|
|
|
@ -15,17 +15,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
// polyfill textencoder if necessary
|
||||
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
||||
let TextEncoder = window.TextEncoder;
|
||||
if (!TextEncoder) {
|
||||
TextEncoder = TextEncodingUtf8.TextEncoder;
|
||||
}
|
||||
let TextDecoder = window.TextDecoder;
|
||||
if (!TextDecoder) {
|
||||
TextDecoder = TextEncodingUtf8.TextDecoder;
|
||||
}
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import SdkConfig from '../SdkConfig';
|
||||
|
||||
|
|
|
@ -19,11 +19,11 @@ limitations under the License.
|
|||
* TODO: Convert this to a real TypeScript interface
|
||||
*/
|
||||
export default class PermalinkConstructor {
|
||||
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
|
||||
forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
|
||||
forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
|
@ -73,12 +73,12 @@ export class PermalinkParts {
|
|||
return new PermalinkParts(null, null, null, groupId, null);
|
||||
}
|
||||
|
||||
static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts {
|
||||
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []);
|
||||
static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts {
|
||||
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers);
|
||||
}
|
||||
|
||||
static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts {
|
||||
return new PermalinkParts(roomId, eventId, null, null, viaServers || []);
|
||||
static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts {
|
||||
return new PermalinkParts(roomId, eventId, null, null, viaServers);
|
||||
}
|
||||
|
||||
get primaryEntityId(): string {
|
||||
|
|
|
@ -149,7 +149,7 @@ export class RoomPermalinkCreator {
|
|||
// Prefer to use canonical alias for permalink if possible
|
||||
const alias = this.room.getCanonicalAlias();
|
||||
if (alias) {
|
||||
return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
|
||||
return getPermalinkConstructor().forRoom(alias);
|
||||
}
|
||||
}
|
||||
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
|
||||
|
@ -302,7 +302,7 @@ export function makeRoomPermalink(roomId: string): string {
|
|||
}
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.load();
|
||||
return permalinkCreator.forRoom();
|
||||
return permalinkCreator.forShareableRoom();
|
||||
}
|
||||
|
||||
export function makeGroupPermalink(groupId: string): string {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue