Merge branch 'develop' into travis/error-states
This commit is contained in:
commit
82119ad595
36 changed files with 1724 additions and 191 deletions
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { HTMLAttributes } from "react";
|
||||
import React, { HTMLAttributes, ReactNode, useContext } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { sortBy } from "lodash";
|
||||
|
@ -24,6 +24,7 @@ import { _t } from "../../../languageHandler";
|
|||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import { useRoomMembers } from "../../../hooks/useRoomMembers";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
const DEFAULT_NUM_FACES = 5;
|
||||
|
||||
|
@ -36,6 +37,7 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
|||
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
|
||||
|
||||
const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
let members = useRoomMembers(room);
|
||||
|
||||
// sort users with an explicit avatar first
|
||||
|
@ -46,21 +48,42 @@ const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, .
|
|||
// sort known users first
|
||||
iteratees.unshift(member => isKnownMember(member));
|
||||
}
|
||||
if (members.length < 1) return null;
|
||||
|
||||
const shownMembers = sortBy(members, iteratees).slice(0, numShown);
|
||||
// exclude ourselves from the shown members list
|
||||
const shownMembers = sortBy(members.filter(m => m.userId !== cli.getUserId()), iteratees).slice(0, numShown);
|
||||
if (shownMembers.length < 1) return null;
|
||||
|
||||
// We reverse the order of the shown faces in CSS to simplify their visual overlap,
|
||||
// reverse members in tooltip order to make the order between the two match up.
|
||||
const commaSeparatedMembers = shownMembers.map(m => m.rawDisplayName).reverse().join(", ");
|
||||
|
||||
let tooltip: ReactNode;
|
||||
if (props.onClick) {
|
||||
tooltip = <div>
|
||||
<div className="mx_Tooltip_title">
|
||||
{ _t("View all %(count)s members", { count: members.length }) }
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{ _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers }) }
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
tooltip = _t("%(count)s members including %(commaSeparatedMembers)s", {
|
||||
count: members.length,
|
||||
commaSeparatedMembers,
|
||||
});
|
||||
}
|
||||
|
||||
return <div {...props} className="mx_FacePile">
|
||||
<div className="mx_FacePile_faces">
|
||||
{ shownMembers.map(member => {
|
||||
return <TextWithTooltip key={member.userId} tooltip={member.name}>
|
||||
<MemberAvatar member={member} width={28} height={28} />
|
||||
</TextWithTooltip>;
|
||||
}) }
|
||||
</div>
|
||||
{ onlyKnownUsers && <span>
|
||||
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}>
|
||||
{ members.length > numShown ? <span className="mx_FacePile_face mx_FacePile_more" /> : null }
|
||||
{ shownMembers.map(m =>
|
||||
<MemberAvatar key={m.userId} member={m} width={28} height={28} className="mx_FacePile_face" /> )}
|
||||
</TextWithTooltip>
|
||||
{ onlyKnownUsers && <span className="mx_FacePile_summary">
|
||||
{ _t("%(count)s people you know have already joined", { count: members.length }) }
|
||||
</span> }
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default FacePile;
|
||||
|
|
|
@ -32,13 +32,14 @@ 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";
|
||||
|
||||
const MIN_ZOOM = 100;
|
||||
const MAX_ZOOM = 300;
|
||||
// This is used for the buttons
|
||||
const ZOOM_STEP = 10;
|
||||
// This is used for mouse wheel events
|
||||
const ZOOM_COEFFICIENT = 7.5;
|
||||
const ZOOM_COEFFICIENT = 0.5;
|
||||
// If we have moved only this much we can zoom
|
||||
const ZOOM_DISTANCE = 10;
|
||||
|
||||
|
@ -115,7 +116,9 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
private onWheel = (ev: WheelEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const newZoom = this.state.zoom - (ev.deltaY * ZOOM_COEFFICIENT);
|
||||
|
||||
const {deltaY} = normalizeWheelEvent(ev);
|
||||
const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT);
|
||||
|
||||
if (newZoom <= MIN_ZOOM) {
|
||||
this.setState({
|
||||
|
|
|
@ -25,6 +25,7 @@ export default class TextWithTooltip extends React.Component {
|
|||
class: PropTypes.string,
|
||||
tooltipClass: PropTypes.string,
|
||||
tooltip: PropTypes.node.isRequired,
|
||||
tooltipProps: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -46,15 +47,17 @@ export default class TextWithTooltip extends React.Component {
|
|||
render() {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
|
||||
const {class: className, children, tooltip, tooltipClass, ...props} = this.props;
|
||||
const {class: className, children, tooltip, tooltipClass, tooltipProps, ...props} = this.props;
|
||||
|
||||
return (
|
||||
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
|
||||
{children}
|
||||
{this.state.hover && <Tooltip
|
||||
{...tooltipProps}
|
||||
label={tooltip}
|
||||
tooltipClassName={tooltipClass}
|
||||
className={"mx_TextWithTooltip_tooltip"} /> }
|
||||
className={"mx_TextWithTooltip_tooltip"}
|
||||
/> }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -545,6 +545,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getUserId();
|
||||
|
||||
let explorePrompt: JSX.Element;
|
||||
if (!this.props.isMinimized) {
|
||||
if (this.state.isNameFiltering) {
|
||||
|
@ -565,21 +568,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (this.props.activeSpace) {
|
||||
} else if (
|
||||
this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join"
|
||||
) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{ _t("Quick actions") }</div>
|
||||
{ this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton
|
||||
{ this.props.activeSpace.canInvite(userId) && <AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_spaceInvite"
|
||||
onClick={this.onSpaceInviteClick}
|
||||
>
|
||||
{_t("Invite people")}
|
||||
</AccessibleButton> }
|
||||
<AccessibleButton
|
||||
{ this.props.activeSpace.getMyMembership() === "join" && <AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_spaceExplore"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{_t("Explore rooms")}
|
||||
</AccessibleButton>
|
||||
</AccessibleButton> }
|
||||
</div>;
|
||||
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
||||
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
||||
|
|
|
@ -53,9 +53,38 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
await this.state.recorder.stop();
|
||||
const mxc = await this.state.recorder.upload();
|
||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||
body: "Voice message",
|
||||
msgtype: "org.matrix.msc2516.voice",
|
||||
url: mxc,
|
||||
"body": "Voice message",
|
||||
"msgtype": "org.matrix.msc2516.voice",
|
||||
//"msgtype": MsgType.Audio,
|
||||
"url": mxc,
|
||||
"info": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
mimetype: this.state.recorder.contentType,
|
||||
size: this.state.recorder.contentLength,
|
||||
},
|
||||
|
||||
// MSC1767 experiment
|
||||
"org.matrix.msc1767.text": "Voice message",
|
||||
"org.matrix.msc1767.file": {
|
||||
url: mxc,
|
||||
name: "Voice message.ogg",
|
||||
mimetype: this.state.recorder.contentType,
|
||||
size: this.state.recorder.contentLength,
|
||||
},
|
||||
"org.matrix.msc1767.audio": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
// TODO: @@ TravisR: Waveform? (MSC1767 decision)
|
||||
},
|
||||
"org.matrix.experimental.msc2516.voice": { // MSC2516+MSC1767 experiment
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
|
||||
// Events can't have floats, so we try to maintain resolution by using 1024
|
||||
// as a maximum value. The waveform contains values between zero and 1, so this
|
||||
// should come out largely sane.
|
||||
//
|
||||
// We're expecting about one data point per second of audio.
|
||||
waveform: this.state.recorder.finalWaveform.map(v => Math.round(v * 1024)),
|
||||
},
|
||||
});
|
||||
await VoiceRecordingStore.instance.disposeRecording();
|
||||
this.setState({recorder: null});
|
||||
|
|
|
@ -25,7 +25,12 @@ import SpaceCreateMenu from "./SpaceCreateMenu";
|
|||
import {SpaceItem} from "./SpaceTreeLevel";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore";
|
||||
import SpaceStore, {
|
||||
HOME_SPACE,
|
||||
UPDATE_INVITED_SPACES,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
UPDATE_TOP_LEVEL_SPACES,
|
||||
} from "../../../stores/SpaceStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
|
@ -105,19 +110,21 @@ const SpaceButton: React.FC<IButtonProps> = ({
|
|||
</li>;
|
||||
}
|
||||
|
||||
const useSpaces = (): [Room[], Room | null] => {
|
||||
const useSpaces = (): [Room[], Room[], Room | null] => {
|
||||
const [invites, setInvites] = useState<Room[]>(SpaceStore.instance.invitedSpaces);
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites);
|
||||
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
|
||||
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
|
||||
return [spaces, activeSpace];
|
||||
return [invites, spaces, activeSpace];
|
||||
};
|
||||
|
||||
const SpacePanel = () => {
|
||||
// We don't need the handle as we position the menu in a constant location
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
||||
const [spaces, activeSpace] = useSpaces();
|
||||
const [invites, spaces, activeSpace] = useSpaces();
|
||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||
|
||||
const newClasses = classNames("mx_SpaceButton_new", {
|
||||
|
@ -209,6 +216,13 @@ const SpacePanel = () => {
|
|||
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
{ invites.map(s => <SpaceItem
|
||||
key={s.roomId}
|
||||
space={s}
|
||||
activeSpaces={activeSpaces}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
onExpand={() => setPanelCollapsed(false)}
|
||||
/>) }
|
||||
{ spaces.map(s => <SpaceItem
|
||||
key={s.roomId}
|
||||
space={s}
|
||||
|
|
|
@ -45,6 +45,8 @@ import RoomViewStore from "../../../stores/RoomViewStore";
|
|||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||
import {NotificationColor} from "../../../stores/notifications/NotificationColor";
|
||||
|
||||
interface IItemProps {
|
||||
space?: Room;
|
||||
|
@ -67,7 +69,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
collapsed: !props.isNested, // default to collapsed for root items
|
||||
collapsed: !props.isNested, // default to collapsed for root items
|
||||
contextMenuPosition: null,
|
||||
};
|
||||
}
|
||||
|
@ -83,6 +85,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
}
|
||||
|
||||
private onContextMenu = (ev: React.MouseEvent) => {
|
||||
if (this.props.space.getMyMembership() !== "join") return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
|
@ -185,6 +188,8 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
};
|
||||
|
||||
private renderContextMenu(): React.ReactElement {
|
||||
if (this.props.space.getMyMembership() !== "join") return null;
|
||||
|
||||
let contextMenu = null;
|
||||
if (this.state.contextMenuPosition) {
|
||||
const userId = this.context.getUserId();
|
||||
|
@ -300,7 +305,9 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
|
||||
mx_SpaceButton_narrow: isNarrow,
|
||||
});
|
||||
const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
|
||||
const notificationState = space.getMyMembership() === "invite"
|
||||
? StaticNotificationState.forSymbol("!", NotificationColor.Red)
|
||||
: SpaceStore.instance.getNotificationState(space.roomId);
|
||||
|
||||
let childItems;
|
||||
if (childSpaces && !collapsed) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue